|
|
@@ -0,0 +1,576 @@
|
|
|
+/**
|
|
|
+ * 临时用电作业票审批流校验
|
|
|
+ *
|
|
|
+ * 用法:node scripts/check-electricity-work-flow-nodes.js
|
|
|
+ *
|
|
|
+ * 校验内容:
|
|
|
+ * 1) 动火 / 受限空间 / 高处作业 / 吊装作业的既有实现文件未被改动
|
|
|
+ * 2) 临时用电 9 个后续环节的 code / name / 字段与源系统一致
|
|
|
+ * 3) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中,且不串台
|
|
|
+ * 4) 默认安全措施清单与源系统 formData 逐条一致
|
|
|
+ * 5) 「风险辨识」与「危害辨识」取值 key 不冲突(源系统两者表单相同)
|
|
|
+ * 6) 临时用电运行时辅助(回显 / payload / 流程变量)的取值口径正确
|
|
|
+ * 7) 组件模板与脚本一致:模板里调用的方法必须都已在组件中定义
|
|
|
+ * (漏定义会在渲染到该分支时抛 "is not a function",整个环节区域渲染不出来)
|
|
|
+ * 8) detail.vue 的作业类型分发与「自动切到后续环节 tab」的时机正确
|
|
|
+ *
|
|
|
+ * 源系统依据:
|
|
|
+ * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
|
|
|
+ * -> 流程定义 HO_PERMIT_ELECTRICITY(id 3766,名称「危险作业-临时用电作业」)
|
|
|
+ * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3766
|
|
|
+ * -> 节点定义(共 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-electricity');
|
|
|
+
|
|
|
+// 其它作业类型的既有实现文件:临时用电实现不得改动它们
|
|
|
+const EXISTING_FILES = [
|
|
|
+ 'fireFlowNodes.js',
|
|
|
+ 'fireFlowTask.js',
|
|
|
+ 'confinedSpaceFlowNodes.js',
|
|
|
+ 'confinedSpaceFlowTask.js',
|
|
|
+ 'heightFlowNodes.js',
|
|
|
+ 'heightFlowTask.js',
|
|
|
+ 'hoistingFlowNodes.js',
|
|
|
+ 'hoistingFlowTask.js',
|
|
|
+ 'components/fireTicketDetail.vue',
|
|
|
+ 'components/flowNodeForm.vue',
|
|
|
+ 'components/confinedSpaceFlow.vue',
|
|
|
+ 'components/heightFlow.vue',
|
|
|
+ 'components/hoistingFlow.vue'
|
|
|
+];
|
|
|
+
|
|
|
+let failed = 0;
|
|
|
+function check(name, cond, extra) {
|
|
|
+ if (cond) {
|
|
|
+ console.log(' PASS ' + name);
|
|
|
+ } else {
|
|
|
+ failed++;
|
|
|
+ console.log(
|
|
|
+ ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 编译 ESM -> CJS
|
|
|
+// electricityFlowTask.js 引用了 fireFlowTask.js(只读复用),
|
|
|
+// fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来
|
|
|
+fs.mkdirSync(OUT, { recursive: true });
|
|
|
+[
|
|
|
+ 'electricityFlowNodes.js',
|
|
|
+ 'electricityFlowTask.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 es = require(path.join(OUT, 'electricityFlowNodes.js'));
|
|
|
+const est = require(path.join(OUT, 'electricityFlowTask.js'));
|
|
|
+
|
|
|
+console.log('\n[1] 其它作业类型的既有实现未被改动');
|
|
|
+EXISTING_FILES.forEach((file) => {
|
|
|
+ let changed = true;
|
|
|
+ try {
|
|
|
+ execFileSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
|
|
|
+ cwd: path.join(SRC, '..'),
|
|
|
+ stdio: 'ignore'
|
|
|
+ });
|
|
|
+ changed = false;
|
|
|
+ } catch (e) {
|
|
|
+ changed = true;
|
|
|
+ }
|
|
|
+ check(file + ' 与 HEAD 一致', !changed);
|
|
|
+});
|
|
|
+
|
|
|
+console.log(
|
|
|
+ '\n[2] 临时用电环节(源系统 HO_PERMIT_ELECTRICITY 共 10 节点,1 级为作业申请模板)'
|
|
|
+);
|
|
|
+const expected = [
|
|
|
+ ['HO_PERMIT_ELECTRICITY_TWOTH', '风险辨识'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_THREETH', '气体分析'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_FOURTH', '危害辨识'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_FIVETH', '现场安全措施确认'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_SIXTH', '安全交底'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_SEVENTH', '作业负责人意见'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_EIGHTTH', '用电单位意见'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_NINETH', '作业实施'],
|
|
|
+ ['HO_PERMIT_ELECTRICITY_TENTH', '完工验收']
|
|
|
+];
|
|
|
+check('环节数量为 9', es.ELECTRICITY_FLOW_NODES.length === 9, {
|
|
|
+ len: es.ELECTRICITY_FLOW_NODES.length
|
|
|
+});
|
|
|
+expected.forEach(([code, name], i) => {
|
|
|
+ const node = es.ELECTRICITY_FLOW_NODES[i];
|
|
|
+ check(
|
|
|
+ '第' + (i + 2) + '级 ' + code + ' / ' + name,
|
|
|
+ node.code === code && node.name === name,
|
|
|
+ { code: node.code, name: node.name }
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+console.log('\n[3] 与源系统 formJson 一致性');
|
|
|
+const nodeOf = (k) => es.ELECTRICITY_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(
|
|
|
+ '气体分析:表名「气体分析记录表」',
|
|
|
+ nodeOf('gas_analysis').fields[0].label === '气体分析记录表'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '气体分析:4 列且顺序与源系统一致(分析时间/分析点/分析结果/分析人)',
|
|
|
+ JSON.stringify(colsOf('gas_analysis', 'gas_analysis_list')) ===
|
|
|
+ JSON.stringify(['分析时间', '分析点', '分析结果', '分析人']),
|
|
|
+ colsOf('gas_analysis', 'gas_analysis_list')
|
|
|
+);
|
|
|
+check(
|
|
|
+ '气体分析:分析时间为日期类型、其余为多行文本(与源系统一致)',
|
|
|
+ nodeOf('gas_analysis').fields[0].columns[0].type === 'date' &&
|
|
|
+ nodeOf('gas_analysis').fields[0].columns[1].type === 'textarea' &&
|
|
|
+ nodeOf('gas_analysis').fields[0].columns[2].type === 'textarea' &&
|
|
|
+ nodeOf('gas_analysis').fields[0].columns[3].type === 'textarea'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '危害辨识:多选 + 单行文本说明(源系统该节点表单复制自风险辨识)',
|
|
|
+ nodeOf('hazard').fields[0].type === 'checkbox' &&
|
|
|
+ nodeOf('hazard').fields[0].options.length === 20 &&
|
|
|
+ nodeOf('hazard').fields[1].type === 'text'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '风险辨识:多选 20 项 + 单行文本说明(源系统 checkbox + input)',
|
|
|
+ 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 === '其它伤害' &&
|
|
|
+ nodeOf('risk').fields[1].type === 'text'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '风险辨识:说明字段别名不与多选框标签冲突',
|
|
|
+ !nodeOf('risk').fields[1].alias.includes('风险辨识结果')
|
|
|
+);
|
|
|
+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(
|
|
|
+ '是否确认选项为 不涉及 / 已确认',
|
|
|
+ JSON.stringify(
|
|
|
+ nodeOf('measure').fields[0].columns[1].options.map((o) => o.label)
|
|
|
+ ) === JSON.stringify(['不涉及', '已确认'])
|
|
|
+);
|
|
|
+check(
|
|
|
+ '安全交底:2 个签字(安全交底人签字 / 接受交底人签字)',
|
|
|
+ JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) ===
|
|
|
+ JSON.stringify(['安全交底人签字', '接受交底人签字'])
|
|
|
+);
|
|
|
+check(
|
|
|
+ '作业负责人意见:意见 + 签字',
|
|
|
+ nodeOf('leader_opinion').fields.length === 2 &&
|
|
|
+ nodeOf('leader_opinion').fields[0].type === 'textarea' &&
|
|
|
+ nodeOf('leader_opinion').fields[1].type === 'sign'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '用电单位意见:意见 + 签字(不是受限空间的「所在单位意见」)',
|
|
|
+ nodeOf('electric_unit_opinion').fields.length === 2 &&
|
|
|
+ nodeOf('electric_unit_opinion').fields[0].type === 'textarea' &&
|
|
|
+ nodeOf('electric_unit_opinion').fields[1].type === 'sign' &&
|
|
|
+ !es.ELECTRICITY_FLOW_NODES.some((n) => n.name === '所在单位意见')
|
|
|
+);
|
|
|
+check(
|
|
|
+ '作业实施:开始时间 + 结束时间 + 事中记录表 + 可燃气体分析记录表',
|
|
|
+ JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) ===
|
|
|
+ JSON.stringify([
|
|
|
+ '作业实施开始时间',
|
|
|
+ '作业实施结束时间',
|
|
|
+ '事中记录表',
|
|
|
+ '可燃气体分析记录表'
|
|
|
+ ])
|
|
|
+);
|
|
|
+check(
|
|
|
+ '事中记录表 3 列(时间/记录内容/记录人)',
|
|
|
+ JSON.stringify(colsOf('execute', 'during_record_list')) ===
|
|
|
+ JSON.stringify(['时间', '记录内容', '记录人'])
|
|
|
+);
|
|
|
+check(
|
|
|
+ '可燃气体分析记录表 4 列(分析时间/分析点/分析结果/分析人)',
|
|
|
+ JSON.stringify(colsOf('execute', 'gas_record_list')) ===
|
|
|
+ JSON.stringify(['分析时间', '分析点', '分析结果', '分析人'])
|
|
|
+);
|
|
|
+check(
|
|
|
+ '完工验收:意见 + 签字',
|
|
|
+ nodeOf('acceptance').fields.length === 2 &&
|
|
|
+ nodeOf('acceptance').fields[0].label === '意见' &&
|
|
|
+ nodeOf('acceptance').fields[1].type === 'sign'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '安全措施默认清单 13 条与源系统 formData 逐条一致',
|
|
|
+ es.DEFAULT_ELECTRICITY_MEASURES.length === 13 &&
|
|
|
+ es.DEFAULT_ELECTRICITY_MEASURES[0] === '作业人员持有电工作业操作证' &&
|
|
|
+ es.DEFAULT_ELECTRICITY_MEASURES[2] ===
|
|
|
+ '上级开关已断电、加锁,并挂安全警示标牌' &&
|
|
|
+ es.DEFAULT_ELECTRICITY_MEASURES[9] ===
|
|
|
+ '临时用电设施已装配漏电保护器,移动工具、手持工具已采取防漏电的安全措施(一机一闸一保护)' &&
|
|
|
+ es.DEFAULT_ELECTRICITY_MEASURES[12] ===
|
|
|
+ '作业场所已进行气体检测且符合作业安全要求'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '默认行挂在 measure_list 字段上(defaultRows)',
|
|
|
+ Array.isArray(nodeOf('measure').fields[0].defaultRows) &&
|
|
|
+ nodeOf('measure').fields[0].defaultRows.length === 13 &&
|
|
|
+ nodeOf('measure').fields[0].defaultRows[0].measure_content ===
|
|
|
+ es.DEFAULT_ELECTRICITY_MEASURES[0]
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n[4] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
|
|
|
+const m = es.matchElectricityFlowNode('HO_PERMIT_ELECTRICITY_THREETH');
|
|
|
+check('按任务定义 key 精确命中', m && m.key === 'gas_analysis', {
|
|
|
+ key: m && m.key
|
|
|
+});
|
|
|
+check(
|
|
|
+ '按节点名称命中',
|
|
|
+ (es.matchElectricityFlowNode('现场安全措施确认') || {}).key === 'measure'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '按别名命中',
|
|
|
+ (es.matchElectricityFlowNode('气体检测') || {}).key === 'gas_analysis'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '按平台自定义 key 反向包含命中',
|
|
|
+ (es.matchElectricityFlowNode('el_gas_analysis') || {}).key === 'gas_analysis'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '流程定义 key / 一级作业申请 key 不误命中后续环节',
|
|
|
+ es.matchElectricityFlowNode('HO_PERMIT_ELECTRICITY') === null &&
|
|
|
+ es.matchElectricityFlowNode('HO_PERMIT_ELECTRICITY_ONETH') === null
|
|
|
+);
|
|
|
+check('空标识返回 null', es.matchElectricityFlowNode('', null) === null);
|
|
|
+check(
|
|
|
+ '「危害辨识」不会被「风险辨识」抢走',
|
|
|
+ (es.matchElectricityFlowNode('危害辨识') || {}).key === 'hazard' &&
|
|
|
+ (es.matchElectricityFlowNode('风险辨识') || {}).key === 'risk'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '「危险辨识」归到风险辨识节点',
|
|
|
+ (es.matchElectricityFlowNode('危险辨识') || {}).key === 'risk'
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n[5] 风险辨识 / 危害辨识 取值 key 不冲突');
|
|
|
+const riskNode = nodeOf('risk');
|
|
|
+const hazardNode = nodeOf('hazard');
|
|
|
+const riskKeys = riskNode.fields.map((f) => f.key);
|
|
|
+const hazardKeys = hazardNode.fields.map((f) => f.key);
|
|
|
+check(
|
|
|
+ '两个环节的字段 key 无交集',
|
|
|
+ riskKeys.every((k) => !hazardKeys.includes(k)),
|
|
|
+ { riskKeys, hazardKeys }
|
|
|
+);
|
|
|
+check(
|
|
|
+ '危害辨识使用 hazard_ 前缀',
|
|
|
+ hazardKeys.every((k) => k.startsWith('hazard_')),
|
|
|
+ hazardKeys
|
|
|
+);
|
|
|
+const hazardAlias = hazardNode.fields.flatMap((f) => f.alias || []);
|
|
|
+check(
|
|
|
+ '危害辨识的别名不会命中风险辨识的取值 key',
|
|
|
+ !hazardAlias.includes('risk_result') &&
|
|
|
+ !hazardAlias.includes('risk_result_desc')
|
|
|
+);
|
|
|
+
|
|
|
+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 = est.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 = est.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 = est.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'
|
|
|
+);
|
|
|
+const riskVars = est.buildNodeVariables(riskNode, {
|
|
|
+ risk_result: ['5', '8'],
|
|
|
+ risk_result_desc: '触电、火灾'
|
|
|
+});
|
|
|
+check(
|
|
|
+ '流程变量:多选值用逗号连接',
|
|
|
+ riskVars.risk_result === '5,8' && riskVars.risk_result_desc === '触电、火灾'
|
|
|
+);
|
|
|
+const hazardVars = est.buildNodeVariables(hazardNode, {
|
|
|
+ hazard_result: ['5'],
|
|
|
+ hazard_result_desc: '触电'
|
|
|
+});
|
|
|
+check(
|
|
|
+ '流程变量:风险辨识与危害辨识各自独立',
|
|
|
+ riskVars.risk_result === '5,8' && hazardVars.hazard_result === '5'
|
|
|
+);
|
|
|
+
|
|
|
+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 {object} { calls, errors }
|
|
|
+ */
|
|
|
+function collectTemplateCalls(template) {
|
|
|
+ const calls = new Set();
|
|
|
+ // 1) 指令 / 事件绑定属性值: :xxx="expr"、v-xxx="expr"、v-xxx:arg="expr"、@xxx="expr"
|
|
|
+ // 2) 插值 {{ 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/electricityFlow.vue');
|
|
|
+const sfcRaw = fs.readFileSync(sfcPath, 'utf8');
|
|
|
+const sfc = compiler.parseComponent(sfcRaw);
|
|
|
+const templateCheck = collectTemplateCalls(sfc.template.content);
|
|
|
+const members = collectComponentMembers(sfc.script.content);
|
|
|
+check(
|
|
|
+ 'electricityFlow.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(便于排查流程模型配置)',
|
|
|
+ templateCheck.calls.has('taskLabel') || /taskLabel/.test(sfcRaw)
|
|
|
+);
|
|
|
+check(
|
|
|
+ '子表日期列渲染为日期时间选择器(el-date-picker)',
|
|
|
+ /<el-date-picker[^>]*column\.type === 'date'/.test(sfcRaw)
|
|
|
+);
|
|
|
+check(
|
|
|
+ '组件引用的是临时用电作业自己的节点/任务文件',
|
|
|
+ /from '\.\.\/electricityFlowNodes'/.test(sfc.script.content) &&
|
|
|
+ /from '\.\.\/electricityFlowTask'/.test(sfc.script.content) &&
|
|
|
+ !/from '\.\.\/heightFlow/.test(sfc.script.content) &&
|
|
|
+ !/from '\.\.\/hoistingFlow/.test(sfc.script.content) &&
|
|
|
+ !/from '\.\.\/confinedSpaceFlow/.test(sfc.script.content)
|
|
|
+);
|
|
|
+check(
|
|
|
+ '组件样式与受限空间实现保持同一套类名(scoped,不影响其它组件)',
|
|
|
+ /current-node-box/.test(sfcRaw) &&
|
|
|
+ /all-nodes-box/.test(sfcRaw) &&
|
|
|
+ /node-form-header/.test(sfcRaw)
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n[8] detail.vue:五类作业的分支互斥且都已接入');
|
|
|
+const detailSrc = fs.readFileSync(path.join(SRC, 'detail.vue'), 'utf8');
|
|
|
+check(
|
|
|
+ 'detail.vue:按作业类型分发到各自的后续环节组件',
|
|
|
+ /isConfinedSpaceWork[\s\S]{0,200}getConfinedTableValue/.test(detailSrc) &&
|
|
|
+ /isHeightWork[\s\S]{0,200}getHeightTableValue/.test(detailSrc) &&
|
|
|
+ /isHeightWork[\s\S]{0,200}saveHeight/.test(detailSrc) &&
|
|
|
+ /isHoistingWork[\s\S]{0,200}getHoistingTableValue/.test(detailSrc) &&
|
|
|
+ /isHoistingWork[\s\S]{0,200}saveHoisting/.test(detailSrc) &&
|
|
|
+ /isElectricityWork[\s\S]{0,200}getElectricityTableValue/.test(detailSrc) &&
|
|
|
+ /isElectricityWork[\s\S]{0,200}saveElectricity/.test(detailSrc)
|
|
|
+);
|
|
|
+check(
|
|
|
+ 'detail.vue:临时用电作业的 tab 与组件已注册',
|
|
|
+ /name="flowElectricity"/.test(detailSrc) &&
|
|
|
+ /<electricity-flow/.test(detailSrc) &&
|
|
|
+ /import ElectricityFlow from '\.\/components\/electricityFlow\.vue'/.test(
|
|
|
+ detailSrc
|
|
|
+ )
|
|
|
+);
|
|
|
+check(
|
|
|
+ 'detail.vue:作业票模板已包含临时用电作业(templates.js key = electricity)',
|
|
|
+ /key:\s*'electricity'/.test(
|
|
|
+ fs.readFileSync(path.join(SRC, 'templates.js'), 'utf8')
|
|
|
+ )
|
|
|
+);
|
|
|
+check(
|
|
|
+ 'detail.vue:自动切 tab 在当前环节识别完成之后执行',
|
|
|
+ /loadCurrentNode\(\)\.then\(\(\) => \{[\s\S]{0,120}switchToFlowTab\(\)/.test(
|
|
|
+ detailSrc
|
|
|
+ ) &&
|
|
|
+ /await this\.loadCurrentNode\(\);[\s\S]{0,120}switchToFlowTab\(\)/.test(
|
|
|
+ detailSrc
|
|
|
+ )
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED'));
|
|
|
+process.exit(failed ? 1 : 0);
|