check-hoisting-work-flow-nodes.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. /**
  2. * 吊装作业票审批流校验
  3. *
  4. * 用法:node scripts/check-hoisting-work-flow-nodes.js
  5. *
  6. * 校验内容:
  7. * 1) 动火作业、受限空间作业、高处作业的既有实现文件未被改动(与 HEAD 完全一致)
  8. * 2) 吊装作业 9 个后续环节的 code / name / 字段与源系统一致
  9. * 3) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
  10. * 4) 默认安全措施清单与源系统 formData 逐条一致
  11. * 5) 吊装作业运行时辅助(回显 / payload / 流程变量)的取值口径正确
  12. * 6) 组件模板与脚本一致:模板里调用的方法必须都已在组件中定义
  13. * (漏定义会在渲染到该分支时抛 "is not a function",整个环节区域渲染不出来)
  14. * 7) detail.vue 的作业类型分发与「自动切到后续环节 tab」的时机正确
  15. *
  16. * 源系统依据:
  17. * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
  18. * -> 流程定义 HO_PERMIT_HOISTING(id 3765,名称「危险作业-吊装作业」,编号前缀 DZTY)
  19. * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3765
  20. * -> 节点定义(共 10 个节点,1 级为作业申请)
  21. */
  22. const fs = require('fs');
  23. const os = require('os');
  24. const path = require('path');
  25. const { execFileSync } = require('child_process');
  26. const babel = require('@babel/core');
  27. const Module = require('module');
  28. const ROOT = path.resolve(__dirname, '..');
  29. const SRC = path.join(
  30. ROOT,
  31. 'src/views/bpm/handleTask/components/dangerousWorkTicket'
  32. );
  33. const OUT = path.join(os.tmpdir(), 'kd-aiot-check-hoisting-work');
  34. // 动火作业、受限空间作业、高处作业的既有实现文件:吊装作业实现不得改动它们
  35. const EXISTING_FILES = [
  36. 'fireFlowNodes.js',
  37. 'fireFlowTask.js',
  38. 'confinedSpaceFlowNodes.js',
  39. 'confinedSpaceFlowTask.js',
  40. 'heightFlowNodes.js',
  41. 'heightFlowTask.js',
  42. 'components/fireTicketDetail.vue',
  43. 'components/flowNodeForm.vue',
  44. 'components/confinedSpaceFlow.vue',
  45. 'components/heightFlow.vue'
  46. ];
  47. let failed = 0;
  48. function check(name, cond, extra) {
  49. if (cond) {
  50. console.log(' PASS ' + name);
  51. } else {
  52. failed++;
  53. console.log(
  54. ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
  55. );
  56. }
  57. }
  58. // 编译 ESM -> CJS
  59. // hoistingFlowTask.js 引用了 fireFlowTask.js(只读复用),
  60. // fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来
  61. fs.mkdirSync(OUT, { recursive: true });
  62. [
  63. 'hoistingFlowNodes.js',
  64. 'hoistingFlowTask.js',
  65. 'fireFlowNodes.js',
  66. 'fireFlowTask.js'
  67. ].forEach((file) => {
  68. const code = fs.readFileSync(path.join(SRC, file), 'utf8');
  69. const res = babel.transformSync(code, {
  70. filename: file,
  71. babelrc: false,
  72. configFile: false,
  73. plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
  74. });
  75. fs.writeFileSync(path.join(OUT, file), res.code);
  76. });
  77. // 吊装作业运行时辅助会引用 @/api/*,这里用桩模块代替,避免校验脚本依赖后端接口
  78. const STUB = path.join(OUT, 'stub.js');
  79. fs.writeFileSync(
  80. STUB,
  81. 'module.exports = {\n' +
  82. ' getTaskListByProcessInstanceId: function () { return Promise.resolve([]); },\n' +
  83. ' getProcessInstance: function () { return Promise.resolve({}); }\n' +
  84. '};\n'
  85. );
  86. const originalResolve = Module._resolveFilename;
  87. Module._resolveFilename = function (request, ...args) {
  88. if (request.startsWith('@/')) {
  89. return STUB;
  90. }
  91. return originalResolve.call(this, request, ...args);
  92. };
  93. const hn = require(path.join(OUT, 'hoistingFlowNodes.js'));
  94. const ht = require(path.join(OUT, 'hoistingFlowTask.js'));
  95. console.log('\n[1] 动火作业、受限空间作业、高处作业既有实现未被改动');
  96. EXISTING_FILES.forEach((file) => {
  97. let changed = true;
  98. try {
  99. execFileSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
  100. cwd: path.join(SRC, '..'),
  101. stdio: 'ignore'
  102. });
  103. changed = false;
  104. } catch (e) {
  105. changed = true;
  106. }
  107. check(file + ' 与 HEAD 一致', !changed);
  108. });
  109. console.log(
  110. '\n[2] 吊装作业环节(源系统 HO_PERMIT_HOISTING 共 10 节点,1 级为作业申请模板)'
  111. );
  112. const expected = [
  113. ['HO_PERMIT_HOISTING_TWOTH', '风险辨识'],
  114. ['HO_PERMIT_HOISTING_THREETH', '现场安全措施确认'],
  115. ['HO_PERMIT_HOISTING_FOURTH', '安全交底'],
  116. ['HO_PERMIT_HOISTING_FIVETH', '作业负责人意见'],
  117. ['HO_PERMIT_HOISTING_SIXTH', '所在单位意见'],
  118. ['HO_PERMIT_HOISTING_SEVENTH', '审核部门意见'],
  119. ['HO_PERMIT_HOISTING_EIGHTTH', '审批部门意见'],
  120. ['HO_PERMIT_HOISTING_NINETH', '作业实施'],
  121. ['HO_PERMIT_HOISTING_TENTH', '完工验收']
  122. ];
  123. check('环节数量为 9', hn.HOISTING_FLOW_NODES.length === 9, {
  124. len: hn.HOISTING_FLOW_NODES.length
  125. });
  126. expected.forEach(([code, name], i) => {
  127. const node = hn.HOISTING_FLOW_NODES[i];
  128. check(
  129. '第' + (i + 2) + '级 ' + code + ' / ' + name,
  130. node.code === code && node.name === name,
  131. { code: node.code, name: node.name }
  132. );
  133. });
  134. console.log('\n[3] 与源系统 formJson 一致性');
  135. const nodeOf = (k) => hn.HOISTING_FLOW_NODES.find((n) => n.key === k);
  136. const colsOf = (k, fk) => {
  137. const node = nodeOf(k);
  138. const field = node.fields.find((f) => f.key === fk);
  139. return (field.columns || []).map((c) => c.label);
  140. };
  141. check(
  142. '风险辨识:结果为单选(源系统 radio,受限空间为多选)',
  143. nodeOf('risk').fields[0].type === 'radio' &&
  144. nodeOf('risk').fields[0].options.length === 20 &&
  145. nodeOf('risk').fields[0].options[0].label === '物体打击' &&
  146. nodeOf('risk').fields[0].options[19].label === '其它伤害'
  147. );
  148. check(
  149. '风险辨识:说明字段为单行文本(源系统 input)',
  150. nodeOf('risk').fields[1].type === 'text'
  151. );
  152. check(
  153. '风险辨识:说明字段别名不与单选框 key 冲突',
  154. !nodeOf('risk').fields[1].alias.includes('risk_result') &&
  155. !nodeOf('risk').fields[1].alias.includes('riskResult')
  156. );
  157. check(
  158. '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)',
  159. nodeOf('measure').fields[0].label === '安全措施确认表' &&
  160. JSON.stringify(colsOf('measure', 'measure_list')) ===
  161. JSON.stringify(['安全措施', '是否确认', '确认人'])
  162. );
  163. check(
  164. '现场安全措施确认:编制人签字 + 确认人签字',
  165. JSON.stringify(nodeOf('measure').fields.map((f) => f.label)) ===
  166. JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字'])
  167. );
  168. check(
  169. '安全交底:安全交底人签字 + 接受交底人签字',
  170. JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) ===
  171. JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
  172. nodeOf('brief').fields.every((f) => f.type === 'sign')
  173. );
  174. check(
  175. '作业负责人意见:意见 + 签名',
  176. nodeOf('leader_opinion').fields.length === 2 &&
  177. nodeOf('leader_opinion').fields[0].type === 'textarea' &&
  178. nodeOf('leader_opinion').fields[1].type === 'sign'
  179. );
  180. check(
  181. '所在单位意见:意见 + 签名',
  182. nodeOf('unit_opinion').fields.length === 2 &&
  183. nodeOf('unit_opinion').fields[0].type === 'textarea' &&
  184. nodeOf('unit_opinion').fields[1].type === 'sign'
  185. );
  186. check(
  187. '审核部门意见:意见 + 签名',
  188. nodeOf('audit_opinion').fields.length === 2 &&
  189. nodeOf('audit_opinion').fields[0].type === 'textarea' &&
  190. nodeOf('audit_opinion').fields[1].type === 'sign'
  191. );
  192. check(
  193. '审批部门意见:意见 + 签名',
  194. nodeOf('approver_opinion').fields.length === 2 &&
  195. nodeOf('approver_opinion').fields[0].type === 'textarea' &&
  196. nodeOf('approver_opinion').fields[1].type === 'sign'
  197. );
  198. check(
  199. '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
  200. JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) ===
  201. JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
  202. );
  203. check(
  204. '事中记录表 3 列(日期时间/记录内容/记录人)',
  205. JSON.stringify(colsOf('execute', 'during_record_list')) ===
  206. JSON.stringify(['日期时间', '记录内容', '记录人'])
  207. );
  208. check(
  209. '完工验收:意见 + 签名',
  210. nodeOf('acceptance').fields.length === 2 &&
  211. nodeOf('acceptance').fields[0].label === '意见' &&
  212. nodeOf('acceptance').fields[1].type === 'sign'
  213. );
  214. // 源系统「现场安全措施确认」节点 formData 的默认 20 条,逐条比对
  215. const SOURCE_MEASURES = [
  216. '一、二级吊装作业已编制吊装作业方案,已经审查批准;吊装物体形状复杂、刚度小,长径比大、精密贵重,作业条件特殊的三级吊装作业,已编制吊装作业方案,已经审查批准',
  217. '吊装场所如有含危险物料的设备、管道时,应制定详细吊装方案,并对设备、管道采取有效防护措施,必要时停车,放空物料,置换后再进行吊装作业',
  218. '作业人员已按规定佩戴个体防护装备',
  219. '已对起重吊装设备、钢丝绳、揽风绳、链条、吊钩等各种机具进行检查,安全可靠',
  220. '已明确各自分工',
  221. '坚守岗位,并统一规定联络信号',
  222. '将建筑物、构筑物作为锚点,应经所属单位工程管理部门审查校算并批准',
  223. '吊装绳索、揽风绳、拖拉绳等不应与带电线路接触,并保持安全距离',
  224. '不应利用管道、管架、电杆、机电设备等作吊装锚点',
  225. '吊物捆扎坚固,未见绳打结、绳不齐现象,棱角吊物已采取衬垫措施',
  226. '起重机安全装置灵活好用',
  227. '吊装作业人员持有有效的法定资格证书',
  228. '地下通信电(光)缆、局域网络电(光)缆,排水沟的益板,承重吊装机械的负重量已确认,保护措施已落实',
  229. '起吊物的质量(t)经确认,在吊装机械的承重范围内',
  230. '在吊装高度的管线、电缆桥架已做好防护措施',
  231. '作业现场围栏、警戒线、警告牌、夜同警示灯已按要求设置',
  232. '作业高度和转臂范围内无架空线路',
  233. '在爆炸危险场所内的作业,机动车排气管已装阻火器',
  234. '露天作业,环境风力满足作业安全要求',
  235. '其他相关特殊作业已办理相应安全作业票'
  236. ];
  237. check(
  238. '安全措施默认清单 20 条与源系统 formData 逐条一致',
  239. JSON.stringify(hn.DEFAULT_HOISTING_MEASURES) ===
  240. JSON.stringify(SOURCE_MEASURES),
  241. {
  242. len: hn.DEFAULT_HOISTING_MEASURES.length,
  243. first: hn.DEFAULT_HOISTING_MEASURES[0],
  244. last: hn.DEFAULT_HOISTING_MEASURES[hn.DEFAULT_HOISTING_MEASURES.length - 1]
  245. }
  246. );
  247. check(
  248. '默认行挂在 measure_list 字段上(defaultRows)',
  249. Array.isArray(nodeOf('measure').fields[0].defaultRows) &&
  250. nodeOf('measure').fields[0].defaultRows.length === 20 &&
  251. nodeOf('measure').fields[0].defaultRows[0].measure_content ===
  252. hn.DEFAULT_HOISTING_MEASURES[0]
  253. );
  254. console.log('\n[4] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
  255. const m = hn.matchHoistingFlowNode('HO_PERMIT_HOISTING_THREETH');
  256. check('按任务定义 key 精确命中', m && m.key === 'measure', {
  257. key: m && m.key
  258. });
  259. check(
  260. '按节点名称命中',
  261. (hn.matchHoistingFlowNode('现场安全措施确认') || {}).key === 'measure'
  262. );
  263. check(
  264. '按别名命中',
  265. (hn.matchHoistingFlowNode('危险辨识') || {}).key === 'risk'
  266. );
  267. check(
  268. '按平台自定义 key 反向包含命中',
  269. (hn.matchHoistingFlowNode('hoisting_risk') || {}).key === 'risk'
  270. );
  271. check(
  272. '流程定义 key / 一级作业申请 key 不误命中后续环节',
  273. hn.matchHoistingFlowNode('HO_PERMIT_HOISTING') === null &&
  274. hn.matchHoistingFlowNode('HO_PERMIT_HOISTING_ONETH') === null
  275. );
  276. check('空标识返回 null', hn.matchHoistingFlowNode('', null) === null);
  277. console.log('\n[5] 运行时辅助(回显 / payload / 流程变量)');
  278. const measureNode = nodeOf('measure');
  279. const source = {
  280. measure_list: [
  281. { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' }
  282. ],
  283. measure_preparer_sign: '/tmp/a.png'
  284. };
  285. const loaded = ht.loadNodeValues(measureNode, [source]);
  286. check(
  287. '回显:按自身 key 取到已保存内容',
  288. loaded.measure_list.length === 1 &&
  289. loaded.measure_preparer_sign === '/tmp/a.png'
  290. );
  291. check(
  292. '回显:未保存的字段为 undefined',
  293. loaded.measure_confirmer_sign === undefined
  294. );
  295. const payload = ht.buildNodePayload(measureNode, {
  296. measure_list: [{ measure_content: 'x' }],
  297. measure_preparer_sign: '/tmp/a.png'
  298. });
  299. check(
  300. 'payload:保留原始类型(数组不转字符串)',
  301. Array.isArray(payload.measure_list) && payload.measure_list.length === 1
  302. );
  303. check(
  304. 'payload:空值不写入',
  305. !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload)
  306. );
  307. const vars = ht.buildNodeVariables(measureNode, {
  308. measure_list: [{ measure_content: 'x' }],
  309. measure_preparer_sign: '/tmp/a.png'
  310. });
  311. check(
  312. '流程变量:子表转 JSON 字符串、其余转字符串',
  313. typeof vars.measure_list === 'string' &&
  314. vars.measure_preparer_sign === '/tmp/a.png'
  315. );
  316. check(
  317. '回显:单选框的值不会串到风险辨识说明',
  318. ht.loadNodeValues(nodeOf('risk'), [{ risk_result: '9' }]).risk_result_desc ===
  319. undefined
  320. );
  321. const briefPayload = ht.buildNodePayload(nodeOf('brief'), {
  322. brief_sign: '/tmp/briefer.png',
  323. brief_receiver_sign: '/tmp/receiver.png'
  324. });
  325. check(
  326. '安全交底:两个签字写入 payload 与流程变量',
  327. briefPayload.brief_sign === '/tmp/briefer.png' &&
  328. briefPayload.brief_receiver_sign === '/tmp/receiver.png' &&
  329. ht.buildNodeVariables(nodeOf('brief'), {
  330. brief_sign: '/tmp/briefer.png'
  331. }).brief_sign === '/tmp/briefer.png'
  332. );
  333. console.log('\n[6] 组件模板与脚本一致性(模板调用的方法必须已在组件中定义)');
  334. const compiler = require('vue-template-compiler');
  335. const parser = require('@babel/parser');
  336. /**
  337. * 收集组件脚本里已定义的成员名(props / data / computed / methods)
  338. * @param {string} script script 源码
  339. * @returns {Set<string>} 成员名集合
  340. */
  341. function collectComponentMembers(script) {
  342. const names = new Set();
  343. const ast = parser.parse(script, {
  344. sourceType: 'module',
  345. plugins: [
  346. 'optionalChaining',
  347. 'objectRestSpread',
  348. 'nullishCoalescingOperator'
  349. ]
  350. });
  351. let component = null;
  352. for (const node of ast.program.body) {
  353. if (node.type === 'ExportDefaultDeclaration') {
  354. component = node.declaration;
  355. }
  356. }
  357. const collectObjectKeys = (obj) => {
  358. ((obj && obj.properties) || []).forEach((prop) => {
  359. if (!prop.key) {
  360. return;
  361. }
  362. names.add(prop.key.name || prop.key.value);
  363. });
  364. };
  365. ((component && component.properties) || []).forEach((prop) => {
  366. if (!prop.key) {
  367. return;
  368. }
  369. const key = prop.key.name;
  370. const value = prop.value;
  371. if (key === 'props' || key === 'methods' || key === 'computed') {
  372. collectObjectKeys(value);
  373. return;
  374. }
  375. if (key === 'data') {
  376. // data() { return { ... } }
  377. const body = value && value.body;
  378. if (!body) {
  379. return;
  380. }
  381. const ret = body.body.find(
  382. (stmt) => stmt.type === 'ReturnStatement' && stmt.argument
  383. );
  384. if (ret) {
  385. collectObjectKeys(ret.argument);
  386. }
  387. }
  388. });
  389. return names;
  390. }
  391. /**
  392. * 取模板里被调用的成员名
  393. *
  394. * 注意:Vue 2 编译后的 render 用 with(this) 解析标识符,
  395. * 方法调用编译成裸函数名(如 getRows(field)),不带 vm. 前缀,
  396. * 因此这里直接扫描模板表达式,而不是扫描编译产物。
  397. *
  398. * @param {string} template 模板源码
  399. * @returns {Set<string>} 被调用的成员名集合
  400. */
  401. function collectTemplateCalls(template) {
  402. const calls = new Set();
  403. // 1) 插值 {{ expr }}
  404. // 2) 指令 / 事件绑定属性值: :xxx="expr"、v-xxx="expr"、v-xxx:arg="expr"、@xxx="expr"
  405. const attrRe = /(?:[:@][\w.-]+|v-[\w-]+(?::[\w.-]+)?)\s*=\s*"([^"]*)"/g;
  406. const interpRe = /\{\{([\s\S]*?)\}\}/g;
  407. const exprs = [];
  408. let m;
  409. while ((m = attrRe.exec(template)) !== null) {
  410. exprs.push(m[1]);
  411. }
  412. while ((m = interpRe.exec(template)) !== null) {
  413. exprs.push(m[1]);
  414. }
  415. // 全局函数与 Vue 内部函数,不属于组件成员
  416. const globals = new Set([
  417. 'String',
  418. 'Number',
  419. 'Boolean',
  420. 'Array',
  421. 'Object',
  422. 'JSON',
  423. 'Math',
  424. 'Date',
  425. 'parseInt',
  426. 'parseFloat',
  427. 'isNaN',
  428. 'require'
  429. ]);
  430. // 标识符后紧跟 "(" 即为函数调用;排除属性访问(xxx.method())与内部变量
  431. const callRe = /(^|[^.\w$])([A-Za-z_$][\w$]*)\s*\(/g;
  432. exprs.forEach((expr) => {
  433. let hit;
  434. callRe.lastIndex = 0;
  435. while ((hit = callRe.exec(expr)) !== null) {
  436. const name = hit[2];
  437. if (name.startsWith('$') || name.startsWith('_') || globals.has(name)) {
  438. continue;
  439. }
  440. calls.add(name);
  441. }
  442. });
  443. return { calls, errors: compiler.compile(template).errors || [] };
  444. }
  445. const sfcPath = path.join(SRC, 'components/hoistingFlow.vue');
  446. const sfcRaw = fs.readFileSync(sfcPath, 'utf8');
  447. const sfc = compiler.parseComponent(sfcRaw);
  448. const templateCheck = collectTemplateCalls(sfc.template.content);
  449. const members = collectComponentMembers(sfc.script.content);
  450. check(
  451. 'hoistingFlow.vue 模板编译无错误',
  452. templateCheck.errors.length === 0,
  453. templateCheck.errors
  454. );
  455. const missing = [...templateCheck.calls].filter((name) => !members.has(name));
  456. check(
  457. `模板调用的 ${templateCheck.calls.size} 个方法均已定义`,
  458. missing.length === 0,
  459. missing
  460. );
  461. check(
  462. '模板确实调用了 getConfirmColumn / confirmAll(子表「全部确认」按钮)',
  463. templateCheck.calls.has('getConfirmColumn') &&
  464. templateCheck.calls.has('confirmAll')
  465. );
  466. check(
  467. '未识别到环节时展示当前任务名/ key(便于排查流程模型配置)',
  468. templateCheck.calls.has('taskLabel') || /taskLabel/.test(sfcRaw)
  469. );
  470. check(
  471. '子表日期列渲染为日期时间选择器(el-date-picker)',
  472. /<el-date-picker[^>]*column\.type === 'date'/.test(sfcRaw)
  473. );
  474. check(
  475. '作业实施:事中记录表「日期时间」列类型为 date',
  476. nodeOf('execute')
  477. .fields.find((f) => f.key === 'during_record_list')
  478. .columns.some((c) => c.key === 'record_time' && c.type === 'date')
  479. );
  480. check(
  481. '组件引用的是吊装作业自己的节点/任务文件',
  482. /from '\.\.\/hoistingFlowNodes'/.test(sfc.script.content) &&
  483. /from '\.\.\/hoistingFlowTask'/.test(sfc.script.content) &&
  484. !/from '\.\.\/heightFlow/.test(sfc.script.content) &&
  485. !/heightFlowNodes|heightFlowTask/.test(sfc.script.content)
  486. );
  487. console.log('\n[7] detail.vue:四类作业的分支互斥且都已接入');
  488. const detailSrc = fs.readFileSync(path.join(SRC, 'detail.vue'), 'utf8');
  489. check(
  490. 'detail.vue:按作业类型分发到各自的后续环节组件',
  491. /isConfinedSpaceWork[\s\S]{0,200}getConfinedTableValue/.test(detailSrc) &&
  492. /isHeightWork[\s\S]{0,200}getHeightTableValue/.test(detailSrc) &&
  493. /isHeightWork[\s\S]{0,200}saveHeight/.test(detailSrc) &&
  494. /isHoistingWork[\s\S]{0,200}getHoistingTableValue/.test(detailSrc) &&
  495. /isHoistingWork[\s\S]{0,200}saveHoisting/.test(detailSrc)
  496. );
  497. check(
  498. 'detail.vue:吊装作业的 tab 与组件已注册',
  499. /name="flowHoisting"/.test(detailSrc) &&
  500. /<hoisting-flow/.test(detailSrc) &&
  501. /import HoistingFlow from '\.\/components\/hoistingFlow\.vue'/.test(
  502. detailSrc
  503. )
  504. );
  505. check(
  506. 'detail.vue:作业票模板已包含吊装作业(templates.js key = hoisting)',
  507. /key:\s*'hoisting'/.test(
  508. fs.readFileSync(path.join(SRC, 'templates.js'), 'utf8')
  509. )
  510. );
  511. check(
  512. 'detail.vue:默认 tab 是「作业票信息」(不再自动切到「后续环节」)',
  513. /activeTab:\s*'info'/.test(detailSrc) &&
  514. !/switchToFlowTab/.test(detailSrc)
  515. );
  516. // 受限空间、高处作业组件只做只读体检,不参与断言(既有实现不做任何改动)
  517. ['components/confinedSpaceFlow.vue', 'components/heightFlow.vue'].forEach(
  518. (file) => {
  519. const otherSfc = compiler.parseComponent(
  520. fs.readFileSync(path.join(SRC, file), 'utf8')
  521. );
  522. const otherCheck = collectTemplateCalls(otherSfc.template.content);
  523. const otherMembers = collectComponentMembers(otherSfc.script.content);
  524. const otherMissing = [...otherCheck.calls].filter(
  525. (name) => !otherMembers.has(name)
  526. );
  527. console.log(
  528. ' INFO ' +
  529. file +
  530. ' 模板调用体检:缺失 ' +
  531. otherMissing.length +
  532. ' 个' +
  533. (otherMissing.length ? ' -> ' + JSON.stringify(otherMissing) : '')
  534. );
  535. }
  536. );
  537. console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED'));
  538. process.exit(failed ? 1 : 0);