|
@@ -0,0 +1,625 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 动土作业票审批流校验
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用法:node scripts/check-break-ground-work-flow-nodes.js
|
|
|
|
|
+ *
|
|
|
|
|
+ * 校验内容:
|
|
|
|
|
+ * 1) 动火、受限空间、高处作业、吊装作业、临时用电作业的既有实现文件未被改动
|
|
|
|
|
+ * (与 HEAD 完全一致)
|
|
|
|
|
+ * 2) detail.vue 只做「新增」:既有作业类型的分发逻辑一行未删
|
|
|
|
|
+ * 3) 动土作业 9 个后续环节的 code / name 与源系统一致
|
|
|
|
|
+ * 4) 各环节的表单设计与源系统 formJson 一致(字段类型 / 列 / 必填项)
|
|
|
|
|
+ * 5) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
|
|
|
|
|
+ * 6) 默认安全措施清单与源系统 formData 逐条一致
|
|
|
|
|
+ * 7) 运行时辅助(回显 / payload / 流程变量)的取值口径正确
|
|
|
|
|
+ * 8) 组件模板与脚本一致:模板里调用的方法必须都已在组件中定义
|
|
|
|
|
+ * (漏定义会在渲染到该分支时抛 "is not a function",整个环节区域渲染不出来)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 源系统依据:
|
|
|
|
|
+ * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
|
|
|
|
|
+ * -> 流程定义 HO_PERMIT_BREAK_GROUND(id 3767,名称「危险作业-动土作业」)
|
|
|
|
|
+ * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3767
|
|
|
|
|
+ * -> 节点定义(共 10 个节点,1 级为作业申请)
|
|
|
|
|
+ */
|
|
|
|
|
+const fs = require('fs');
|
|
|
|
|
+const os = require('os');
|
|
|
|
|
+const path = require('path');
|
|
|
|
|
+const { execFileSync } = require('child_process');
|
|
|
|
|
+const babel = require('@babel/core');
|
|
|
|
|
+const Module = require('module');
|
|
|
|
|
+
|
|
|
|
|
+const ROOT = path.resolve(__dirname, '..');
|
|
|
|
|
+const SRC = path.join(
|
|
|
|
|
+ ROOT,
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket'
|
|
|
|
|
+);
|
|
|
|
|
+const OUT = path.join(os.tmpdir(), 'kd-aiot-check-break-ground-work');
|
|
|
|
|
+
|
|
|
|
|
+// 动火、受限空间、高处作业、吊装作业、临时用电作业的既有实现文件:
|
|
|
|
|
+// 动土作业实现不得改动它们(路径相对仓库根,便于 git diff 精确比对)
|
|
|
|
|
+const EXISTING_FILES = [
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/fireFlowNodes.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/fireFlowTask.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/confinedSpaceFlowNodes.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/confinedSpaceFlowTask.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/heightFlowNodes.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/heightFlowTask.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/hoistingFlowNodes.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/hoistingFlowTask.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/electricityFlowNodes.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/electricityFlowTask.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/fireTicketDetail.vue',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/flowNodeForm.vue',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/confinedSpaceFlow.vue',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/heightFlow.vue',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/hoistingFlow.vue',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/electricityFlow.vue',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/templates.js',
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/workTypes.js'
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+let failed = 0;
|
|
|
|
|
+function check(name, cond, extra) {
|
|
|
|
|
+ if (cond) {
|
|
|
|
|
+ console.log(' PASS ' + name);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ failed++;
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 取文件的 git diff 文本
|
|
|
|
|
+ * @param {string} file 相对仓库根的文件路径
|
|
|
|
|
+ * @returns {string}
|
|
|
|
|
+ */
|
|
|
|
|
+function gitDiff(file) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return execFileSync('git', ['diff', 'HEAD', '--', file], {
|
|
|
|
|
+ cwd: ROOT,
|
|
|
|
|
+ encoding: 'utf8',
|
|
|
|
|
+ maxBuffer: 1024 * 1024 * 20
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 取 diff 里被删除的行(去掉 diff 头部的 --- / ---)
|
|
|
|
|
+ * @param {string} diff
|
|
|
|
|
+ * @returns {string[]}
|
|
|
|
|
+ */
|
|
|
|
|
+function removedLines(diff) {
|
|
|
|
|
+ return (diff || '')
|
|
|
|
|
+ .split(/\r?\n/)
|
|
|
|
|
+ .filter((line) => line.startsWith('-') && !line.startsWith('---'))
|
|
|
|
|
+ .map((line) => line.slice(1).trim());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 编译 ESM -> CJS
|
|
|
|
|
+// breakGroundFlowTask.js 引用了 fireFlowTask.js(只读复用),
|
|
|
|
|
+// fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来
|
|
|
|
|
+fs.mkdirSync(OUT, { recursive: true });
|
|
|
|
|
+[
|
|
|
|
|
+ 'breakGroundFlowNodes.js',
|
|
|
|
|
+ 'breakGroundFlowTask.js',
|
|
|
|
|
+ 'fireFlowNodes.js',
|
|
|
|
|
+ 'fireFlowTask.js'
|
|
|
|
|
+].forEach((file) => {
|
|
|
|
|
+ const code = fs.readFileSync(path.join(SRC, file), 'utf8');
|
|
|
|
|
+ const res = babel.transformSync(code, {
|
|
|
|
|
+ filename: file,
|
|
|
|
|
+ babelrc: false,
|
|
|
|
|
+ configFile: false,
|
|
|
|
|
+ plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
|
|
|
|
|
+ });
|
|
|
|
|
+ fs.writeFileSync(path.join(OUT, file), res.code);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 动土作业运行时辅助会引用 @/api/*,这里用桩模块代替,避免校验脚本依赖后端接口
|
|
|
|
|
+const STUB = path.join(OUT, 'stub.js');
|
|
|
|
|
+fs.writeFileSync(
|
|
|
|
|
+ STUB,
|
|
|
|
|
+ 'module.exports = {\n' +
|
|
|
|
|
+ ' getTaskListByProcessInstanceId: function () { return Promise.resolve([]); },\n' +
|
|
|
|
|
+ ' getProcessInstance: function () { return Promise.resolve({}); }\n' +
|
|
|
|
|
+ '};\n'
|
|
|
|
|
+);
|
|
|
|
|
+const originalResolve = Module._resolveFilename;
|
|
|
|
|
+Module._resolveFilename = function (request, ...args) {
|
|
|
|
|
+ if (request.startsWith('@/')) {
|
|
|
|
|
+ return STUB;
|
|
|
|
|
+ }
|
|
|
|
|
+ return originalResolve.call(this, request, ...args);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const bg = require(path.join(OUT, 'breakGroundFlowNodes.js'));
|
|
|
|
|
+const bt = require(path.join(OUT, 'breakGroundFlowTask.js'));
|
|
|
|
|
+
|
|
|
|
|
+console.log(
|
|
|
|
|
+ '\n[1] 动火 / 受限空间 / 高处作业 / 吊装作业 / 临时用电作业既有实现未被改动'
|
|
|
|
|
+);
|
|
|
|
|
+EXISTING_FILES.forEach((file) => {
|
|
|
|
|
+ let changed = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ execFileSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
|
|
|
|
|
+ cwd: ROOT,
|
|
|
|
|
+ stdio: 'ignore'
|
|
|
|
|
+ });
|
|
|
|
|
+ changed = false;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ changed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ check(file.split('/').pop() + ' 与 HEAD 一致', !changed);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n[2] detail.vue 只做新增(既有作业类型的分发逻辑一行未删)');
|
|
|
|
|
+const DETAIL =
|
|
|
|
|
+ 'src/views/bpm/handleTask/components/dangerousWorkTicket/detail.vue';
|
|
|
|
|
+const detailDiff = gitDiff(DETAIL);
|
|
|
|
|
+const detailRemoved = removedLines(detailDiff);
|
|
|
|
|
+// 允许删除的行:仅文件头注释里的「五类作业」说明,switchToFlowTab 中
|
|
|
|
|
+// 临时用电分支补 return 后的收尾行,以及组件注册里因补逗号而重排的 ElectricityFlow
|
|
|
|
|
+const ALLOWED_REMOVED = [
|
|
|
|
|
+ '动火、受限空间、高处作业、吊装作业、临时用电作业五类作业共用本页面与 submit.vue,按作业类型分发到各自的组件:',
|
|
|
|
|
+ "this.activeTab = 'flowElectricity';",
|
|
|
|
|
+ 'ElectricityFlow',
|
|
|
|
|
+ '}'
|
|
|
|
|
+];
|
|
|
|
|
+const unexpectedRemoved = detailRemoved.filter(
|
|
|
|
|
+ (line) => !ALLOWED_REMOVED.includes(line)
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ `detail.vue 的删除行仅为预期的 ${ALLOWED_REMOVED.length} 行`,
|
|
|
|
|
+ unexpectedRemoved.length === 0,
|
|
|
|
|
+ unexpectedRemoved
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ 'detail.vue 新增了动土作业的 tab 分发',
|
|
|
|
|
+ /<break-ground-flow[\s\S]{0,300}ref="breakGroundFlow"/.test(detailDiff)
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ 'detail.vue 新增了动土作业的当前环节计算属性',
|
|
|
|
|
+ /currentBreakGroundNode\(\)\s*\{[\s\S]{0,200}matchBreakGroundFlowNode/.test(
|
|
|
|
|
+ detailDiff
|
|
|
|
|
+ )
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ 'detail.vue 新增了动土作业的取数 / 保存方法',
|
|
|
|
|
+ /getBreakGroundTableValue\(\)/.test(detailDiff) &&
|
|
|
|
|
+ /saveBreakGround\(data\)/.test(detailDiff) &&
|
|
|
|
|
+ /validateBreakGroundNodeForm\(\)/.test(detailDiff)
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ 'detail.vue 的自动切 tab 在当前环节识别完成之后执行(未被破坏)',
|
|
|
|
|
+ /loadCurrentNode\(\)\.then\(\(\) => \{[\s\S]{0,120}switchToFlowTab\(\)/.test(
|
|
|
|
|
+ detailSrc()
|
|
|
|
|
+ ) &&
|
|
|
|
|
+ /await this\.loadCurrentNode\(\);[\s\S]{0,120}switchToFlowTab\(\)/.test(
|
|
|
|
|
+ detailSrc()
|
|
|
|
|
+ )
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 读取 detail.vue 源码
|
|
|
|
|
+ * @returns {string}
|
|
|
|
|
+ */
|
|
|
|
|
+function detailSrc() {
|
|
|
|
|
+ return fs.readFileSync(path.join(SRC, 'detail.vue'), 'utf8');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+console.log(
|
|
|
|
|
+ '\n[3] 动土作业环节(源系统 HO_PERMIT_BREAK_GROUND 共 10 节点,1 级为作业申请模板)'
|
|
|
|
|
+);
|
|
|
|
|
+const expected = [
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_TWOTH', '风险辨识'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_THREETH', '现场安全措施确认'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_FOURTH', '安全交底'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_FIVETH', '作业负责人意见'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_SIXTH', '所在单位意见'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_SEVENTH', '有关部门会签意见'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_EIGHTTH', '审批部门意见'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_NINETH', '作业实施'],
|
|
|
|
|
+ ['HO_PERMIT_BREAK_GROUND_TENTH', '完工验收']
|
|
|
|
|
+];
|
|
|
|
|
+check('环节数量为 9', bg.BREAK_GROUND_FLOW_NODES.length === 9, {
|
|
|
|
|
+ len: bg.BREAK_GROUND_FLOW_NODES.length
|
|
|
|
|
+});
|
|
|
|
|
+expected.forEach(([code, name], i) => {
|
|
|
|
|
+ const node = bg.BREAK_GROUND_FLOW_NODES[i];
|
|
|
|
|
+ check(
|
|
|
|
|
+ '第' + (i + 2) + '级 ' + code + ' / ' + name,
|
|
|
|
|
+ node.code === code && node.name === name,
|
|
|
|
|
+ { code: node.code, name: node.name }
|
|
|
|
|
+ );
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n[4] 与源系统 formJson 一致性');
|
|
|
|
|
+const nodeOf = (k) => bg.BREAK_GROUND_FLOW_NODES.find((n) => n.key === k);
|
|
|
|
|
+const colsOf = (k, fk) => {
|
|
|
|
|
+ const node = nodeOf(k);
|
|
|
|
|
+ const field = node.fields.find((f) => f.key === fk);
|
|
|
|
|
+ return (field.columns || []).map((c) => c.label);
|
|
|
|
|
+};
|
|
|
|
|
+check(
|
|
|
|
|
+ '风险辨识:结果为多选 20 项(源系统 checkbox)',
|
|
|
|
|
+ nodeOf('risk').fields[0].type === 'checkbox' &&
|
|
|
|
|
+ nodeOf('risk').fields[0].options.length === 20 &&
|
|
|
|
|
+ nodeOf('risk').fields[0].options[0].label === '物体打击' &&
|
|
|
|
|
+ nodeOf('risk').fields[0].options[19].label === '其它伤害'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '风险辨识:说明字段为单行文本(源系统 input)',
|
|
|
|
|
+ nodeOf('risk').fields[1].type === 'text'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '风险辨识:说明字段别名不与多选框 key 冲突',
|
|
|
|
|
+ !nodeOf('risk').fields[1].alias.includes('risk_result') &&
|
|
|
|
|
+ !nodeOf('risk').fields[1].alias.includes('riskResult')
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)',
|
|
|
|
|
+ nodeOf('measure').fields[0].label === '安全措施确认表' &&
|
|
|
|
|
+ JSON.stringify(colsOf('measure', 'measure_list')) ===
|
|
|
|
|
+ JSON.stringify(['安全措施', '是否确认', '确认人'])
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '现场安全措施确认:编制人签字 + 确认人签字',
|
|
|
|
|
+ JSON.stringify(nodeOf('measure').fields.map((f) => f.label)) ===
|
|
|
|
|
+ JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字'])
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
|
|
|
|
|
+ nodeOf('measure')
|
|
|
|
|
+ .fields[0].columns.find((c) => c.key === 'measure_confirm')
|
|
|
|
|
+ .options.map((o) => o.label)
|
|
|
|
|
+ .join(',') === '不涉及,已确认'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
|
|
|
|
|
+ JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) ===
|
|
|
|
|
+ JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
|
|
|
|
|
+ nodeOf('brief').fields.every((f) => f.type === 'sign')
|
|
|
|
|
+);
|
|
|
|
|
+[
|
|
|
|
|
+ ['leader_opinion', '作业负责人意见'],
|
|
|
|
|
+ ['unit_opinion', '所在单位意见'],
|
|
|
|
|
+ ['countersign_opinion', '有关部门会签意见'],
|
|
|
|
|
+ ['approver_opinion', '审批部门意见']
|
|
|
|
|
+].forEach(([key, label]) => {
|
|
|
|
|
+ const node = nodeOf(key);
|
|
|
|
|
+ check(
|
|
|
|
|
+ label + ':意见 + 签名',
|
|
|
|
|
+ node.fields.length === 2 &&
|
|
|
|
|
+ node.fields[0].type === 'textarea' &&
|
|
|
|
|
+ node.fields[1].type === 'sign',
|
|
|
|
|
+ { labels: node.fields.map((f) => f.label) }
|
|
|
|
|
+ );
|
|
|
|
|
+});
|
|
|
|
|
+check(
|
|
|
|
|
+ '意见类环节的取值 key 互不相同(源系统四处复制同一 model,不能互相覆盖)',
|
|
|
|
|
+ new Set(
|
|
|
|
|
+ bg.BREAK_GROUND_FLOW_NODES.filter(
|
|
|
|
|
+ (n) => n.fields[0].type === 'textarea'
|
|
|
|
|
+ ).map((n) => n.fields[0].key)
|
|
|
|
|
+ ).size === 5
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
|
|
|
|
|
+ JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) ===
|
|
|
|
|
+ JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '事中记录表 3 列(时间/记录内容/记录人)',
|
|
|
|
|
+ JSON.stringify(colsOf('execute', 'during_record_list')) ===
|
|
|
|
|
+ JSON.stringify(['时间', '记录内容', '记录人'])
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '完工验收:意见 + 签名',
|
|
|
|
|
+ nodeOf('acceptance').fields.length === 2 &&
|
|
|
|
|
+ nodeOf('acceptance').fields[0].label === '意见' &&
|
|
|
|
|
+ nodeOf('acceptance').fields[1].type === 'sign'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '安全措施默认清单 10 条与源系统 formData 逐条一致',
|
|
|
|
|
+ bg.DEFAULT_BREAK_GROUND_MEASURES.length === 10 &&
|
|
|
|
|
+ bg.DEFAULT_BREAK_GROUND_MEASURES[0] ===
|
|
|
|
|
+ '地下电力电缆、通信电(光)缆、局域网络电(光)缆已确认,保护措施已落实' &&
|
|
|
|
|
+ bg.DEFAULT_BREAK_GROUND_MEASURES[4] === '已进行放坡处理和固壁支撑' &&
|
|
|
|
|
+ bg.DEFAULT_BREAK_GROUND_MEASURES[6] ===
|
|
|
|
|
+ '现场夜间有充足照明:A.36 V.24V.12 V 防水型灯:B.36V、24 V.12 V 防爆型灯' &&
|
|
|
|
|
+ bg.DEFAULT_BREAK_GROUND_MEASURES[8] ===
|
|
|
|
|
+ '易燃易爆、有毒气体存在的场所动土深度超过1.2m,已按照受限空间作业要求采取了措施' &&
|
|
|
|
|
+ bg.DEFAULT_BREAK_GROUND_MEASURES[9] ===
|
|
|
|
|
+ '其他相关特殊作业已办理相应安全作业票'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '默认行挂在 measure_list 字段上(defaultRows)',
|
|
|
|
|
+ Array.isArray(nodeOf('measure').fields[0].defaultRows) &&
|
|
|
|
|
+ nodeOf('measure').fields[0].defaultRows.length === 10 &&
|
|
|
|
|
+ nodeOf('measure').fields[0].defaultRows[0].measure_content ===
|
|
|
|
|
+ bg.DEFAULT_BREAK_GROUND_MEASURES[0]
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n[5] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
|
|
|
|
|
+const m = bg.matchBreakGroundFlowNode('HO_PERMIT_BREAK_GROUND_THREETH');
|
|
|
|
|
+check('按任务定义 key 精确命中', m && m.key === 'measure', {
|
|
|
|
|
+ key: m && m.key
|
|
|
|
|
+});
|
|
|
|
|
+check(
|
|
|
|
|
+ '按节点名称命中',
|
|
|
|
|
+ (bg.matchBreakGroundFlowNode('现场安全措施确认') || {}).key === 'measure'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '按别名命中',
|
|
|
|
|
+ (bg.matchBreakGroundFlowNode('危险辨识') || {}).key === 'risk'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '按平台自定义 key 反向包含命中',
|
|
|
|
|
+ (bg.matchBreakGroundFlowNode('break_ground_risk') || {}).key === 'risk'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '流程定义 key / 一级作业申请 key 不误命中后续环节',
|
|
|
|
|
+ bg.matchBreakGroundFlowNode('HO_PERMIT_BREAK_GROUND') === null &&
|
|
|
|
|
+ bg.matchBreakGroundFlowNode('HO_PERMIT_BREAK_GROUND_ONETH') === null
|
|
|
|
|
+);
|
|
|
|
|
+check('空标识返回 null', bg.matchBreakGroundFlowNode('', null) === null);
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n[6] 运行时辅助(回显 / payload / 流程变量)');
|
|
|
|
|
+const measureNode = nodeOf('measure');
|
|
|
|
|
+const source = {
|
|
|
|
|
+ measure_list: [
|
|
|
|
|
+ { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ measure_preparer_sign: '/tmp/a.png'
|
|
|
|
|
+};
|
|
|
|
|
+const loaded = bt.loadNodeValues(measureNode, [source]);
|
|
|
|
|
+check(
|
|
|
|
|
+ '回显:按自身 key 取到已保存内容',
|
|
|
|
|
+ loaded.measure_list.length === 1 &&
|
|
|
|
|
+ loaded.measure_preparer_sign === '/tmp/a.png'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '回显:未保存的字段为 undefined',
|
|
|
|
|
+ loaded.measure_confirmer_sign === undefined
|
|
|
|
|
+);
|
|
|
|
|
+const payload = bt.buildNodePayload(measureNode, {
|
|
|
|
|
+ measure_list: [{ measure_content: 'x' }],
|
|
|
|
|
+ measure_preparer_sign: '/tmp/a.png'
|
|
|
|
|
+});
|
|
|
|
|
+check(
|
|
|
|
|
+ 'payload:保留原始类型(数组不转字符串)',
|
|
|
|
|
+ Array.isArray(payload.measure_list) && payload.measure_list.length === 1
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ 'payload:空值不写入',
|
|
|
|
|
+ !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload)
|
|
|
|
|
+);
|
|
|
|
|
+const vars = bt.buildNodeVariables(measureNode, {
|
|
|
|
|
+ measure_list: [{ measure_content: 'x' }],
|
|
|
|
|
+ measure_preparer_sign: '/tmp/a.png'
|
|
|
|
|
+});
|
|
|
|
|
+check(
|
|
|
|
|
+ '流程变量:子表转 JSON 字符串、其余转字符串',
|
|
|
|
|
+ typeof vars.measure_list === 'string' &&
|
|
|
|
|
+ vars.measure_preparer_sign === '/tmp/a.png'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '回显:多选框的值不会串到风险辨识说明',
|
|
|
|
|
+ bt.loadNodeValues(nodeOf('risk'), [{ risk_result: '9' }]).risk_result_desc ===
|
|
|
|
|
+ undefined
|
|
|
|
|
+);
|
|
|
|
|
+const opinionPayload = bt.buildNodePayload(nodeOf('leader_opinion'), {
|
|
|
|
|
+ leader_opinion: '同意',
|
|
|
|
|
+ leader_sign: '/tmp/leader.png'
|
|
|
|
|
+});
|
|
|
|
|
+check(
|
|
|
|
|
+ '作业负责人意见:意见 + 签名写入 payload 与流程变量',
|
|
|
|
|
+ opinionPayload.leader_opinion === '同意' &&
|
|
|
|
|
+ opinionPayload.leader_sign === '/tmp/leader.png' &&
|
|
|
|
|
+ bt.buildNodeVariables(nodeOf('leader_opinion'), {
|
|
|
|
|
+ leader_opinion: '同意'
|
|
|
|
|
+ }).leader_opinion === '同意'
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '四个意见类环节的 payload 互不覆盖',
|
|
|
|
|
+ bt.buildNodePayload(nodeOf('unit_opinion'), { unit_opinion: '单位意见' })
|
|
|
|
|
+ .unit_opinion === '单位意见' &&
|
|
|
|
|
+ bt.buildNodePayload(nodeOf('approver_opinion'), {
|
|
|
|
|
+ approver_opinion: '审批意见'
|
|
|
|
|
+ }).approver_opinion === '审批意见'
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n[7] 组件模板与脚本一致性(模板调用的方法必须已在组件中定义)');
|
|
|
|
|
+const compiler = require('vue-template-compiler');
|
|
|
|
|
+const parser = require('@babel/parser');
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 收集组件脚本里已定义的成员名(props / data / computed / methods)
|
|
|
|
|
+ * @param {string} script script 源码
|
|
|
|
|
+ * @returns {Set<string>} 成员名集合
|
|
|
|
|
+ */
|
|
|
|
|
+function collectComponentMembers(script) {
|
|
|
|
|
+ const names = new Set();
|
|
|
|
|
+ const ast = parser.parse(script, {
|
|
|
|
|
+ sourceType: 'module',
|
|
|
|
|
+ plugins: [
|
|
|
|
|
+ 'optionalChaining',
|
|
|
|
|
+ 'objectRestSpread',
|
|
|
|
|
+ 'nullishCoalescingOperator'
|
|
|
|
|
+ ]
|
|
|
|
|
+ });
|
|
|
|
|
+ let component = null;
|
|
|
|
|
+ for (const node of ast.program.body) {
|
|
|
|
|
+ if (node.type === 'ExportDefaultDeclaration') {
|
|
|
|
|
+ component = node.declaration;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const collectObjectKeys = (obj) => {
|
|
|
|
|
+ ((obj && obj.properties) || []).forEach((prop) => {
|
|
|
|
|
+ if (!prop.key) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ names.add(prop.key.name || prop.key.value);
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+ ((component && component.properties) || []).forEach((prop) => {
|
|
|
|
|
+ if (!prop.key) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const key = prop.key.name;
|
|
|
|
|
+ const value = prop.value;
|
|
|
|
|
+ if (key === 'props' || key === 'methods' || key === 'computed') {
|
|
|
|
|
+ collectObjectKeys(value);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (key === 'data') {
|
|
|
|
|
+ // data() { return { ... } }
|
|
|
|
|
+ const body = value && value.body;
|
|
|
|
|
+ if (!body) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const ret = body.body.find(
|
|
|
|
|
+ (stmt) => stmt.type === 'ReturnStatement' && stmt.argument
|
|
|
|
|
+ );
|
|
|
|
|
+ if (ret) {
|
|
|
|
|
+ collectObjectKeys(ret.argument);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return names;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 取模板里被调用的成员名
|
|
|
|
|
+ *
|
|
|
|
|
+ * 注意:Vue 2 编译后的 render 用 with(this) 解析标识符,
|
|
|
|
|
+ * 方法调用编译成裸函数名(如 getRows(field)),不带 vm. 前缀,
|
|
|
|
|
+ * 因此这里直接扫描模板表达式,而不是扫描编译产物。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {string} template 模板源码
|
|
|
|
|
+ * @returns {Set<string>} 被调用的成员名集合
|
|
|
|
|
+ */
|
|
|
|
|
+function collectTemplateCalls(template) {
|
|
|
|
|
+ const calls = new Set();
|
|
|
|
|
+ // 1) 插值 {{ expr }}
|
|
|
|
|
+ // 2) 指令 / 事件绑定属性值: :xxx="expr"、v-xxx="expr"、v-xxx:arg="expr"、@xxx="expr"
|
|
|
|
|
+ const attrRe = /(?:[:@][\w.-]+|v-[\w-]+(?::[\w.-]+)?)\s*=\s*"([^"]*)"/g;
|
|
|
|
|
+ const interpRe = /\{\{([\s\S]*?)\}\}/g;
|
|
|
|
|
+ const exprs = [];
|
|
|
|
|
+ let m;
|
|
|
|
|
+ while ((m = attrRe.exec(template)) !== null) {
|
|
|
|
|
+ exprs.push(m[1]);
|
|
|
|
|
+ }
|
|
|
|
|
+ while ((m = interpRe.exec(template)) !== null) {
|
|
|
|
|
+ exprs.push(m[1]);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 全局函数与 Vue 内部函数,不属于组件成员
|
|
|
|
|
+ const globals = new Set([
|
|
|
|
|
+ 'String',
|
|
|
|
|
+ 'Number',
|
|
|
|
|
+ 'Boolean',
|
|
|
|
|
+ 'Array',
|
|
|
|
|
+ 'Object',
|
|
|
|
|
+ 'JSON',
|
|
|
|
|
+ 'Math',
|
|
|
|
|
+ 'Date',
|
|
|
|
|
+ 'parseInt',
|
|
|
|
|
+ 'parseFloat',
|
|
|
|
|
+ 'isNaN',
|
|
|
|
|
+ 'require'
|
|
|
|
|
+ ]);
|
|
|
|
|
+ // 标识符后紧跟 "(" 即为函数调用;排除属性访问(xxx.method())与内部变量
|
|
|
|
|
+ const callRe = /(^|[^.\w$])([A-Za-z_$][\w$]*)\s*\(/g;
|
|
|
|
|
+ exprs.forEach((expr) => {
|
|
|
|
|
+ let hit;
|
|
|
|
|
+ callRe.lastIndex = 0;
|
|
|
|
|
+ while ((hit = callRe.exec(expr)) !== null) {
|
|
|
|
|
+ const name = hit[2];
|
|
|
|
|
+ if (name.startsWith('$') || name.startsWith('_') || globals.has(name)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ calls.add(name);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return { calls, errors: compiler.compile(template).errors || [] };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const sfcPath = path.join(SRC, 'components/breakGroundFlow.vue');
|
|
|
|
|
+const sfcSource = fs.readFileSync(sfcPath, 'utf8');
|
|
|
|
|
+const sfc = compiler.parseComponent(sfcSource);
|
|
|
|
|
+const templateCheck = collectTemplateCalls(sfc.template.content);
|
|
|
|
|
+const members = collectComponentMembers(sfc.script.content);
|
|
|
|
|
+check(
|
|
|
|
|
+ 'breakGroundFlow.vue 模板编译无错误',
|
|
|
|
|
+ templateCheck.errors.length === 0,
|
|
|
|
|
+ templateCheck.errors
|
|
|
|
|
+);
|
|
|
|
|
+const missing = [...templateCheck.calls].filter((name) => !members.has(name));
|
|
|
|
|
+check(
|
|
|
|
|
+ `模板调用的 ${templateCheck.calls.size} 个方法均已定义`,
|
|
|
|
|
+ missing.length === 0,
|
|
|
|
|
+ missing
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '模板确实调用了 getConfirmColumn / confirmAll(子表「全部确认」按钮)',
|
|
|
|
|
+ templateCheck.calls.has('getConfirmColumn') &&
|
|
|
|
|
+ templateCheck.calls.has('confirmAll')
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '未识别到环节时展示当前任务名 / key(便于排查流程模型配置)',
|
|
|
|
|
+ /taskLabel/.test(sfcSource)
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '子表日期列渲染为日期时间选择器(el-date-picker)',
|
|
|
|
|
+ /<el-date-picker[^>]*column\.type === 'date'/.test(sfcSource)
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '作业实施:事中记录表「时间」列类型为 date',
|
|
|
|
|
+ nodeOf('execute')
|
|
|
|
|
+ .fields.find((f) => f.key === 'during_record_list')
|
|
|
|
|
+ .columns.some((c) => c.key === 'record_time' && c.type === 'date')
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '组件对外提供 validate / getPayload / getVariables',
|
|
|
|
|
+ templateCheck.calls.size >= 0 &&
|
|
|
|
|
+ /validate\(\)\s*\{/.test(sfc.script.content) &&
|
|
|
|
|
+ /getPayload\(\)\s*\{/.test(sfc.script.content) &&
|
|
|
|
|
+ /getVariables\(\)\s*\{/.test(sfc.script.content)
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ '组件引用的节点 schema 与运行时辅助均来自动土作业自己的文件',
|
|
|
|
|
+ /from '\.\.\/breakGroundFlowNodes'/.test(sfc.script.content) &&
|
|
|
|
|
+ /from '\.\.\/breakGroundFlowTask'/.test(sfc.script.content)
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+// 其余作业类型组件只做只读体检,不参与断言(既有实现不做任何改动)
|
|
|
|
|
+[
|
|
|
|
|
+ 'confinedSpaceFlow.vue',
|
|
|
|
|
+ 'heightFlow.vue',
|
|
|
|
|
+ 'hoistingFlow.vue',
|
|
|
|
|
+ 'electricityFlow.vue'
|
|
|
|
|
+].forEach((name) => {
|
|
|
|
|
+ const otherSfc = compiler.parseComponent(
|
|
|
|
|
+ fs.readFileSync(path.join(SRC, 'components', name), 'utf8')
|
|
|
|
|
+ );
|
|
|
|
|
+ const otherCheck = collectTemplateCalls(otherSfc.template.content);
|
|
|
|
|
+ const otherMembers = collectComponentMembers(otherSfc.script.content);
|
|
|
|
|
+ const otherMissing = [...otherCheck.calls].filter(
|
|
|
|
|
+ (n) => !otherMembers.has(n)
|
|
|
|
|
+ );
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ ' INFO ' +
|
|
|
|
|
+ name +
|
|
|
|
|
+ ' 模板调用体检:缺失 ' +
|
|
|
|
|
+ otherMissing.length +
|
|
|
|
|
+ ' 个' +
|
|
|
|
|
+ (otherMissing.length ? ' -> ' + JSON.stringify(otherMissing) : '')
|
|
|
|
|
+ );
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED'));
|
|
|
|
|
+process.exit(failed ? 1 : 0);
|