|
|
@@ -0,0 +1,560 @@
|
|
|
+/**
|
|
|
+ * 吊装作业票审批流校验
|
|
|
+ *
|
|
|
+ * 用法:node scripts/check-hoisting-work-flow-nodes.js
|
|
|
+ *
|
|
|
+ * 校验内容:
|
|
|
+ * 1) 动火作业、受限空间作业、高处作业的既有实现文件未被改动(与 HEAD 完全一致)
|
|
|
+ * 2) 吊装作业 9 个后续环节的 code / name / 字段与源系统一致
|
|
|
+ * 3) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
|
|
|
+ * 4) 默认安全措施清单与源系统 formData 逐条一致
|
|
|
+ * 5) 吊装作业运行时辅助(回显 / payload / 流程变量)的取值口径正确
|
|
|
+ * 6) 组件模板与脚本一致:模板里调用的方法必须都已在组件中定义
|
|
|
+ * (漏定义会在渲染到该分支时抛 "is not a function",整个环节区域渲染不出来)
|
|
|
+ * 7) detail.vue 的作业类型分发与「自动切到后续环节 tab」的时机正确
|
|
|
+ *
|
|
|
+ * 源系统依据:
|
|
|
+ * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
|
|
|
+ * -> 流程定义 HO_PERMIT_HOISTING(id 3765,名称「危险作业-吊装作业」,编号前缀 DZTY)
|
|
|
+ * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3765
|
|
|
+ * -> 节点定义(共 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-hoisting-work');
|
|
|
+
|
|
|
+// 动火作业、受限空间作业、高处作业的既有实现文件:吊装作业实现不得改动它们
|
|
|
+const EXISTING_FILES = [
|
|
|
+ 'fireFlowNodes.js',
|
|
|
+ 'fireFlowTask.js',
|
|
|
+ 'confinedSpaceFlowNodes.js',
|
|
|
+ 'confinedSpaceFlowTask.js',
|
|
|
+ 'heightFlowNodes.js',
|
|
|
+ 'heightFlowTask.js',
|
|
|
+ 'components/fireTicketDetail.vue',
|
|
|
+ 'components/flowNodeForm.vue',
|
|
|
+ 'components/confinedSpaceFlow.vue',
|
|
|
+ 'components/heightFlow.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
|
|
|
+// hoistingFlowTask.js 引用了 fireFlowTask.js(只读复用),
|
|
|
+// fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来
|
|
|
+fs.mkdirSync(OUT, { recursive: true });
|
|
|
+[
|
|
|
+ 'hoistingFlowNodes.js',
|
|
|
+ 'hoistingFlowTask.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 hn = require(path.join(OUT, 'hoistingFlowNodes.js'));
|
|
|
+const ht = require(path.join(OUT, 'hoistingFlowTask.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_HOISTING 共 10 节点,1 级为作业申请模板)'
|
|
|
+);
|
|
|
+const expected = [
|
|
|
+ ['HO_PERMIT_HOISTING_TWOTH', '风险辨识'],
|
|
|
+ ['HO_PERMIT_HOISTING_THREETH', '现场安全措施确认'],
|
|
|
+ ['HO_PERMIT_HOISTING_FOURTH', '安全交底'],
|
|
|
+ ['HO_PERMIT_HOISTING_FIVETH', '作业负责人意见'],
|
|
|
+ ['HO_PERMIT_HOISTING_SIXTH', '所在单位意见'],
|
|
|
+ ['HO_PERMIT_HOISTING_SEVENTH', '审核部门意见'],
|
|
|
+ ['HO_PERMIT_HOISTING_EIGHTTH', '审批部门意见'],
|
|
|
+ ['HO_PERMIT_HOISTING_NINETH', '作业实施'],
|
|
|
+ ['HO_PERMIT_HOISTING_TENTH', '完工验收']
|
|
|
+];
|
|
|
+check('环节数量为 9', hn.HOISTING_FLOW_NODES.length === 9, {
|
|
|
+ len: hn.HOISTING_FLOW_NODES.length
|
|
|
+});
|
|
|
+expected.forEach(([code, name], i) => {
|
|
|
+ const node = hn.HOISTING_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) => hn.HOISTING_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(
|
|
|
+ '风险辨识:结果为单选(源系统 radio,受限空间为多选)',
|
|
|
+ nodeOf('risk').fields[0].type === 'radio' &&
|
|
|
+ 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(
|
|
|
+ '安全交底:安全交底人签字 + 接受交底人签字',
|
|
|
+ JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) ===
|
|
|
+ JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
|
|
|
+ nodeOf('brief').fields.every((f) => f.type === 'sign')
|
|
|
+);
|
|
|
+check(
|
|
|
+ '作业负责人意见:意见 + 签名',
|
|
|
+ nodeOf('leader_opinion').fields.length === 2 &&
|
|
|
+ nodeOf('leader_opinion').fields[0].type === 'textarea' &&
|
|
|
+ nodeOf('leader_opinion').fields[1].type === 'sign'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '所在单位意见:意见 + 签名',
|
|
|
+ nodeOf('unit_opinion').fields.length === 2 &&
|
|
|
+ nodeOf('unit_opinion').fields[0].type === 'textarea' &&
|
|
|
+ nodeOf('unit_opinion').fields[1].type === 'sign'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '审核部门意见:意见 + 签名',
|
|
|
+ nodeOf('audit_opinion').fields.length === 2 &&
|
|
|
+ nodeOf('audit_opinion').fields[0].type === 'textarea' &&
|
|
|
+ nodeOf('audit_opinion').fields[1].type === 'sign'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '审批部门意见:意见 + 签名',
|
|
|
+ nodeOf('approver_opinion').fields.length === 2 &&
|
|
|
+ nodeOf('approver_opinion').fields[0].type === 'textarea' &&
|
|
|
+ nodeOf('approver_opinion').fields[1].type === 'sign'
|
|
|
+);
|
|
|
+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'
|
|
|
+);
|
|
|
+
|
|
|
+// 源系统「现场安全措施确认」节点 formData 的默认 20 条,逐条比对
|
|
|
+const SOURCE_MEASURES = [
|
|
|
+ '一、二级吊装作业已编制吊装作业方案,已经审查批准;吊装物体形状复杂、刚度小,长径比大、精密贵重,作业条件特殊的三级吊装作业,已编制吊装作业方案,已经审查批准',
|
|
|
+ '吊装场所如有含危险物料的设备、管道时,应制定详细吊装方案,并对设备、管道采取有效防护措施,必要时停车,放空物料,置换后再进行吊装作业',
|
|
|
+ '作业人员已按规定佩戴个体防护装备',
|
|
|
+ '已对起重吊装设备、钢丝绳、揽风绳、链条、吊钩等各种机具进行检查,安全可靠',
|
|
|
+ '已明确各自分工',
|
|
|
+ '坚守岗位,并统一规定联络信号',
|
|
|
+ '将建筑物、构筑物作为锚点,应经所属单位工程管理部门审查校算并批准',
|
|
|
+ '吊装绳索、揽风绳、拖拉绳等不应与带电线路接触,并保持安全距离',
|
|
|
+ '不应利用管道、管架、电杆、机电设备等作吊装锚点',
|
|
|
+ '吊物捆扎坚固,未见绳打结、绳不齐现象,棱角吊物已采取衬垫措施',
|
|
|
+ '起重机安全装置灵活好用',
|
|
|
+ '吊装作业人员持有有效的法定资格证书',
|
|
|
+ '地下通信电(光)缆、局域网络电(光)缆,排水沟的益板,承重吊装机械的负重量已确认,保护措施已落实',
|
|
|
+ '起吊物的质量(t)经确认,在吊装机械的承重范围内',
|
|
|
+ '在吊装高度的管线、电缆桥架已做好防护措施',
|
|
|
+ '作业现场围栏、警戒线、警告牌、夜同警示灯已按要求设置',
|
|
|
+ '作业高度和转臂范围内无架空线路',
|
|
|
+ '在爆炸危险场所内的作业,机动车排气管已装阻火器',
|
|
|
+ '露天作业,环境风力满足作业安全要求',
|
|
|
+ '其他相关特殊作业已办理相应安全作业票'
|
|
|
+];
|
|
|
+check(
|
|
|
+ '安全措施默认清单 20 条与源系统 formData 逐条一致',
|
|
|
+ JSON.stringify(hn.DEFAULT_HOISTING_MEASURES) ===
|
|
|
+ JSON.stringify(SOURCE_MEASURES),
|
|
|
+ {
|
|
|
+ len: hn.DEFAULT_HOISTING_MEASURES.length,
|
|
|
+ first: hn.DEFAULT_HOISTING_MEASURES[0],
|
|
|
+ last: hn.DEFAULT_HOISTING_MEASURES[hn.DEFAULT_HOISTING_MEASURES.length - 1]
|
|
|
+ }
|
|
|
+);
|
|
|
+check(
|
|
|
+ '默认行挂在 measure_list 字段上(defaultRows)',
|
|
|
+ Array.isArray(nodeOf('measure').fields[0].defaultRows) &&
|
|
|
+ nodeOf('measure').fields[0].defaultRows.length === 20 &&
|
|
|
+ nodeOf('measure').fields[0].defaultRows[0].measure_content ===
|
|
|
+ hn.DEFAULT_HOISTING_MEASURES[0]
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n[4] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
|
|
|
+const m = hn.matchHoistingFlowNode('HO_PERMIT_HOISTING_THREETH');
|
|
|
+check('按任务定义 key 精确命中', m && m.key === 'measure', {
|
|
|
+ key: m && m.key
|
|
|
+});
|
|
|
+check(
|
|
|
+ '按节点名称命中',
|
|
|
+ (hn.matchHoistingFlowNode('现场安全措施确认') || {}).key === 'measure'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '按别名命中',
|
|
|
+ (hn.matchHoistingFlowNode('危险辨识') || {}).key === 'risk'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '按平台自定义 key 反向包含命中',
|
|
|
+ (hn.matchHoistingFlowNode('hoisting_risk') || {}).key === 'risk'
|
|
|
+);
|
|
|
+check(
|
|
|
+ '流程定义 key / 一级作业申请 key 不误命中后续环节',
|
|
|
+ hn.matchHoistingFlowNode('HO_PERMIT_HOISTING') === null &&
|
|
|
+ hn.matchHoistingFlowNode('HO_PERMIT_HOISTING_ONETH') === null
|
|
|
+);
|
|
|
+check('空标识返回 null', hn.matchHoistingFlowNode('', null) === null);
|
|
|
+
|
|
|
+console.log('\n[5] 运行时辅助(回显 / 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 = ht.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 = ht.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 = ht.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(
|
|
|
+ '回显:单选框的值不会串到风险辨识说明',
|
|
|
+ ht.loadNodeValues(nodeOf('risk'), [{ risk_result: '9' }]).risk_result_desc ===
|
|
|
+ undefined
|
|
|
+);
|
|
|
+const briefPayload = ht.buildNodePayload(nodeOf('brief'), {
|
|
|
+ brief_sign: '/tmp/briefer.png',
|
|
|
+ brief_receiver_sign: '/tmp/receiver.png'
|
|
|
+});
|
|
|
+check(
|
|
|
+ '安全交底:两个签字写入 payload 与流程变量',
|
|
|
+ briefPayload.brief_sign === '/tmp/briefer.png' &&
|
|
|
+ briefPayload.brief_receiver_sign === '/tmp/receiver.png' &&
|
|
|
+ ht.buildNodeVariables(nodeOf('brief'), {
|
|
|
+ brief_sign: '/tmp/briefer.png'
|
|
|
+ }).brief_sign === '/tmp/briefer.png'
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n[6] 组件模板与脚本一致性(模板调用的方法必须已在组件中定义)');
|
|
|
+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/hoistingFlow.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(
|
|
|
+ 'hoistingFlow.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(
|
|
|
+ '作业实施:事中记录表「日期时间」列类型为 date',
|
|
|
+ nodeOf('execute')
|
|
|
+ .fields.find((f) => f.key === 'during_record_list')
|
|
|
+ .columns.some((c) => c.key === 'record_time' && c.type === 'date')
|
|
|
+);
|
|
|
+check(
|
|
|
+ '组件引用的是吊装作业自己的节点/任务文件',
|
|
|
+ /from '\.\.\/hoistingFlowNodes'/.test(sfc.script.content) &&
|
|
|
+ /from '\.\.\/hoistingFlowTask'/.test(sfc.script.content) &&
|
|
|
+ !/from '\.\.\/heightFlow/.test(sfc.script.content) &&
|
|
|
+ !/heightFlowNodes|heightFlowTask/.test(sfc.script.content)
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n[7] 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)
|
|
|
+);
|
|
|
+check(
|
|
|
+ 'detail.vue:吊装作业的 tab 与组件已注册',
|
|
|
+ /name="flowHoisting"/.test(detailSrc) &&
|
|
|
+ /<hoisting-flow/.test(detailSrc) &&
|
|
|
+ /import HoistingFlow from '\.\/components\/hoistingFlow\.vue'/.test(
|
|
|
+ detailSrc
|
|
|
+ )
|
|
|
+);
|
|
|
+check(
|
|
|
+ 'detail.vue:作业票模板已包含吊装作业(templates.js key = hoisting)',
|
|
|
+ /key:\s*'hoisting'/.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
|
|
|
+ )
|
|
|
+);
|
|
|
+// 受限空间、高处作业组件只做只读体检,不参与断言(既有实现不做任何改动)
|
|
|
+['components/confinedSpaceFlow.vue', 'components/heightFlow.vue'].forEach(
|
|
|
+ (file) => {
|
|
|
+ const otherSfc = compiler.parseComponent(
|
|
|
+ fs.readFileSync(path.join(SRC, file), 'utf8')
|
|
|
+ );
|
|
|
+ const otherCheck = collectTemplateCalls(otherSfc.template.content);
|
|
|
+ const otherMembers = collectComponentMembers(otherSfc.script.content);
|
|
|
+ const otherMissing = [...otherCheck.calls].filter(
|
|
|
+ (name) => !otherMembers.has(name)
|
|
|
+ );
|
|
|
+ console.log(
|
|
|
+ ' INFO ' +
|
|
|
+ file +
|
|
|
+ ' 模板调用体检:缺失 ' +
|
|
|
+ otherMissing.length +
|
|
|
+ ' 个' +
|
|
|
+ (otherMissing.length ? ' -> ' + JSON.stringify(otherMissing) : '')
|
|
|
+ );
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED'));
|
|
|
+process.exit(failed ? 1 : 0);
|