check-break-road-work-flow-nodes.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. /**
  2. * 断路作业票审批流校验
  3. *
  4. * 用法:node scripts/check-break-road-work-flow-nodes.js
  5. *
  6. * 校验内容:
  7. * 1) 动火、受限空间、高处作业、吊装作业、临时用电作业、动土作业的既有实现文件未被改动
  8. * (与 HEAD 完全一致)
  9. * 2) detail.vue 只做「新增」:既有作业类型的分发逻辑一行未删
  10. * 3) 断路作业 9 个后续环节的 code / name 与源系统一致
  11. * 4) 各环节的表单设计与源系统 formJson 一致(字段类型 / 列 / 必填项)
  12. * 5) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
  13. * 6) 默认安全措施清单与源系统 formData 逐条一致
  14. * 7) 运行时辅助(回显 / payload / 流程变量)的取值口径正确
  15. * 8) 组件模板与脚本一致:模板里调用的方法必须都已在组件中定义
  16. * (漏定义会在渲染到该分支时抛 "is not a function",整个环节区域渲染不出来)
  17. *
  18. * 源系统依据:
  19. * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
  20. * -> 流程定义 HO_PERMIT_BREAK_ROAD(id 3768,名称「危险作业-断路作业」,编号前缀 DLZY)
  21. * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3768
  22. * -> 节点定义(共 10 个节点,1 级为作业申请)
  23. */
  24. const fs = require('fs');
  25. const os = require('os');
  26. const path = require('path');
  27. const { execFileSync } = require('child_process');
  28. const babel = require('@babel/core');
  29. const Module = require('module');
  30. const ROOT = path.resolve(__dirname, '..');
  31. const SRC = path.join(
  32. ROOT,
  33. 'src/views/bpm/handleTask/components/dangerousWorkTicket'
  34. );
  35. const OUT = path.join(os.tmpdir(), 'kd-aiot-check-break-road-work');
  36. // 既有作业类型的实现文件:断路作业实现不得改动它们
  37. // (路径相对仓库根,便于 git diff 精确比对)
  38. const EXISTING_FILES = [
  39. 'src/views/bpm/handleTask/components/dangerousWorkTicket/fireFlowNodes.js',
  40. 'src/views/bpm/handleTask/components/dangerousWorkTicket/fireFlowTask.js',
  41. 'src/views/bpm/handleTask/components/dangerousWorkTicket/confinedSpaceFlowNodes.js',
  42. 'src/views/bpm/handleTask/components/dangerousWorkTicket/confinedSpaceFlowTask.js',
  43. 'src/views/bpm/handleTask/components/dangerousWorkTicket/heightFlowNodes.js',
  44. 'src/views/bpm/handleTask/components/dangerousWorkTicket/heightFlowTask.js',
  45. 'src/views/bpm/handleTask/components/dangerousWorkTicket/hoistingFlowNodes.js',
  46. 'src/views/bpm/handleTask/components/dangerousWorkTicket/hoistingFlowTask.js',
  47. 'src/views/bpm/handleTask/components/dangerousWorkTicket/electricityFlowNodes.js',
  48. 'src/views/bpm/handleTask/components/dangerousWorkTicket/electricityFlowTask.js',
  49. 'src/views/bpm/handleTask/components/dangerousWorkTicket/breakGroundFlowNodes.js',
  50. 'src/views/bpm/handleTask/components/dangerousWorkTicket/breakGroundFlowTask.js',
  51. 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/fireTicketDetail.vue',
  52. 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/flowNodeForm.vue',
  53. 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/confinedSpaceFlow.vue',
  54. 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/heightFlow.vue',
  55. 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/hoistingFlow.vue',
  56. 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/electricityFlow.vue',
  57. 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/breakGroundFlow.vue',
  58. 'src/views/bpm/handleTask/components/dangerousWorkTicket/templates.js',
  59. 'src/views/bpm/handleTask/components/dangerousWorkTicket/workTypes.js'
  60. ];
  61. let failed = 0;
  62. function check(name, cond, extra) {
  63. if (cond) {
  64. console.log(' PASS ' + name);
  65. } else {
  66. failed++;
  67. console.log(
  68. ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
  69. );
  70. }
  71. }
  72. /**
  73. * 取文件的 git diff 文本
  74. * @param {string} file 相对仓库根的文件路径
  75. * @returns {string}
  76. */
  77. function gitDiff(file) {
  78. try {
  79. return execFileSync('git', ['diff', 'HEAD', '--', file], {
  80. cwd: ROOT,
  81. encoding: 'utf8',
  82. maxBuffer: 1024 * 1024 * 20
  83. });
  84. } catch (e) {
  85. return '';
  86. }
  87. }
  88. /**
  89. * 取 diff 里被删除的行(去掉 diff 头部的 --- / ---)
  90. * @param {string} diff
  91. * @returns {string[]}
  92. */
  93. function removedLines(diff) {
  94. return (diff || '')
  95. .split(/\r?\n/)
  96. .filter((line) => line.startsWith('-') && !line.startsWith('---'))
  97. .map((line) => line.slice(1).trim());
  98. }
  99. /**
  100. * 读取 detail.vue 源码
  101. * @returns {string}
  102. */
  103. function detailSrc() {
  104. return fs.readFileSync(path.join(SRC, 'detail.vue'), 'utf8');
  105. }
  106. // 编译 ESM -> CJS
  107. // breakRoadFlowTask.js 引用了 fireFlowTask.js(只读复用),
  108. // fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来
  109. fs.mkdirSync(OUT, { recursive: true });
  110. [
  111. 'breakRoadFlowNodes.js',
  112. 'breakRoadFlowTask.js',
  113. 'fireFlowNodes.js',
  114. 'fireFlowTask.js'
  115. ].forEach((file) => {
  116. const code = fs.readFileSync(path.join(SRC, file), 'utf8');
  117. const res = babel.transformSync(code, {
  118. filename: file,
  119. babelrc: false,
  120. configFile: false,
  121. plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
  122. });
  123. fs.writeFileSync(path.join(OUT, file), res.code);
  124. });
  125. // 断路作业运行时辅助会引用 @/api/*,这里用桩模块代替,避免校验脚本依赖后端接口
  126. const STUB = path.join(OUT, 'stub.js');
  127. fs.writeFileSync(
  128. STUB,
  129. 'module.exports = {\n' +
  130. ' getTaskListByProcessInstanceId: function () { return Promise.resolve([]); },\n' +
  131. ' getProcessInstance: function () { return Promise.resolve({}); }\n' +
  132. '};\n'
  133. );
  134. const originalResolve = Module._resolveFilename;
  135. Module._resolveFilename = function (request, ...args) {
  136. if (request.startsWith('@/')) {
  137. return STUB;
  138. }
  139. return originalResolve.call(this, request, ...args);
  140. };
  141. const br = require(path.join(OUT, 'breakRoadFlowNodes.js'));
  142. const bt = require(path.join(OUT, 'breakRoadFlowTask.js'));
  143. console.log(
  144. '\n[1] 动火 / 受限空间 / 高处作业 / 吊装作业 / 临时用电作业 / 动土作业既有实现未被改动'
  145. );
  146. EXISTING_FILES.forEach((file) => {
  147. let changed = true;
  148. try {
  149. execFileSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
  150. cwd: ROOT,
  151. stdio: 'ignore'
  152. });
  153. changed = false;
  154. } catch (e) {
  155. changed = true;
  156. }
  157. check(file.split('/').pop() + ' 与 HEAD 一致', !changed);
  158. });
  159. console.log('\n[2] detail.vue 只做新增(既有作业类型的分发逻辑一行未删)');
  160. const DETAIL =
  161. 'src/views/bpm/handleTask/components/dangerousWorkTicket/detail.vue';
  162. const detailDiff = gitDiff(DETAIL);
  163. const detailRemoved = removedLines(detailDiff);
  164. // 允许删除的行:仅文件头注释里的「六类作业」说明,
  165. // 以及组件注册里因补逗号而重排的 BreakGroundFlow
  166. const ALLOWED_REMOVED = [
  167. '动火、受限空间、高处作业、吊装作业、临时用电作业、动土作业六类作业共用本页面与 submit.vue,按作业类型分发到各自的组件:',
  168. 'BreakGroundFlow'
  169. ];
  170. const unexpectedRemoved = detailRemoved.filter(
  171. (line) => !ALLOWED_REMOVED.includes(line)
  172. );
  173. check(
  174. `detail.vue 的删除行仅为预期的 ${ALLOWED_REMOVED.length} 行`,
  175. unexpectedRemoved.length === 0,
  176. unexpectedRemoved
  177. );
  178. check(
  179. 'detail.vue 新增了断路作业的 tab 分发',
  180. /<break-road-flow[\s\S]{0,300}ref="breakRoadFlow"/.test(detailDiff)
  181. );
  182. check(
  183. 'detail.vue 新增了断路作业的当前环节计算属性',
  184. /currentBreakRoadNode\(\)\s*\{[\s\S]{0,200}matchBreakRoadFlowNode/.test(
  185. detailDiff
  186. )
  187. );
  188. check(
  189. 'detail.vue 新增了断路作业的取数 / 保存方法',
  190. /getBreakRoadTableValue\(\)/.test(detailDiff) &&
  191. /saveBreakRoad\(data\)/.test(detailDiff) &&
  192. /validateBreakRoadNodeForm\(\)/.test(detailDiff)
  193. );
  194. check(
  195. 'detail.vue 注册了断路作业组件',
  196. /import BreakRoadFlow from '\.\/components\/breakRoadFlow\.vue'/.test(
  197. detailSrc()
  198. ) && /BreakRoadFlow/.test(detailSrc())
  199. );
  200. check(
  201. 'detail.vue 的默认 tab 是「作业票信息」(不再自动切到「后续环节」)',
  202. /activeTab:\s*'info'/.test(detailSrc()) &&
  203. !/switchToFlowTab/.test(detailSrc())
  204. );
  205. console.log(
  206. '\n[3] 断路作业环节(源系统 HO_PERMIT_BREAK_ROAD 共 10 节点,1 级为作业申请模板)'
  207. );
  208. const expected = [
  209. ['HO_PERMIT_BREAK_ROAD_TWOTH', '风险辨识'],
  210. ['HO_PERMIT_BREAK_ROAD_THREETH', '现场安全措施确认'],
  211. ['HO_PERMIT_BREAK_ROAD_FOURTH', '安全交底'],
  212. ['HO_PERMIT_BREAK_ROAD_FIVETH', '作业负责人意见'],
  213. ['HO_PERMIT_BREAK_ROAD_SIXTH', '所在单位意见'],
  214. ['HO_PERMIT_BREAK_ROAD_SEVENTH', '消防、安全部门意见'],
  215. ['HO_PERMIT_BREAK_ROAD_EIGHTTH', '审批部门意见'],
  216. ['HO_PERMIT_BREAK_ROAD_NINETH', '作业实施'],
  217. ['HO_PERMIT_BREAK_ROAD_TENTH', '完工验收']
  218. ];
  219. check('环节数量为 9', br.BREAK_ROAD_FLOW_NODES.length === 9, {
  220. len: br.BREAK_ROAD_FLOW_NODES.length
  221. });
  222. expected.forEach(([code, name], i) => {
  223. const node = br.BREAK_ROAD_FLOW_NODES[i];
  224. check(
  225. '第' + (i + 2) + '级 ' + code + ' / ' + name,
  226. node.code === code && node.name === name,
  227. { code: node.code, name: node.name }
  228. );
  229. });
  230. console.log('\n[4] 与源系统 formJson 一致性');
  231. const nodeOf = (k) => br.BREAK_ROAD_FLOW_NODES.find((n) => n.key === k);
  232. const colsOf = (k, fk) => {
  233. const node = nodeOf(k);
  234. const field = node.fields.find((f) => f.key === fk);
  235. return (field.columns || []).map((c) => c.label);
  236. };
  237. check(
  238. '风险辨识:结果为多选 20 项(源系统 checkbox)',
  239. nodeOf('risk').fields[0].type === 'checkbox' &&
  240. nodeOf('risk').fields[0].options.length === 20 &&
  241. nodeOf('risk').fields[0].options[0].label === '物体打击' &&
  242. nodeOf('risk').fields[0].options[19].label === '其它伤害'
  243. );
  244. check(
  245. '风险辨识:说明字段为多行文本(源系统 textarea)',
  246. nodeOf('risk').fields[1].type === 'textarea'
  247. );
  248. check(
  249. '风险辨识:说明字段别名不与多选框 key 冲突',
  250. !nodeOf('risk').fields[1].alias.includes('risk_result') &&
  251. !nodeOf('risk').fields[1].alias.includes('riskResult')
  252. );
  253. check(
  254. '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)',
  255. nodeOf('measure').fields[0].label === '安全措施确认表' &&
  256. JSON.stringify(colsOf('measure', 'measure_list')) ===
  257. JSON.stringify(['安全措施', '是否确认', '确认人'])
  258. );
  259. check(
  260. '现场安全措施确认:编制人签字 + 确认人签字',
  261. JSON.stringify(nodeOf('measure').fields.map((f) => f.label)) ===
  262. JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字'])
  263. );
  264. check(
  265. '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
  266. nodeOf('measure')
  267. .fields[0].columns.find((c) => c.key === 'measure_confirm')
  268. .options.map((o) => o.label)
  269. .join(',') === '不涉及,已确认'
  270. );
  271. check(
  272. '现场安全措施确认:确认人列为单行文本(源系统 input)',
  273. nodeOf('measure').fields[0].columns.find((c) => c.key === 'measure_confirmer')
  274. .type === 'text'
  275. );
  276. check(
  277. '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
  278. JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) ===
  279. JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
  280. nodeOf('brief').fields.every((f) => f.type === 'sign')
  281. );
  282. [
  283. ['leader_opinion', '作业负责人意见'],
  284. ['unit_opinion', '所在单位意见']
  285. ].forEach(([key, label]) => {
  286. const node = nodeOf(key);
  287. check(
  288. label + ':意见 + 签名',
  289. node.fields.length === 2 &&
  290. node.fields[0].type === 'textarea' &&
  291. node.fields[1].type === 'sign',
  292. { labels: node.fields.map((f) => f.label) }
  293. );
  294. });
  295. check(
  296. '消防、安全部门意见:两个意见框 + 两个签字(消防部门 / 安全部门)',
  297. JSON.stringify(nodeOf('fire_safety_opinion').fields.map((f) => f.label)) ===
  298. JSON.stringify([
  299. '消防部门意见',
  300. '安全部门意见',
  301. '消防部门签字',
  302. '安全部门签字'
  303. ]) &&
  304. nodeOf('fire_safety_opinion').fields[0].type === 'textarea' &&
  305. nodeOf('fire_safety_opinion').fields[1].type === 'textarea' &&
  306. nodeOf('fire_safety_opinion').fields[2].type === 'sign' &&
  307. nodeOf('fire_safety_opinion').fields[3].type === 'sign'
  308. );
  309. // 签字项标题要能分辨出是谁签的:
  310. // 同一环节里不允许出现两个同名的「手写签名」,且标题沿用源系统的原始写法
  311. check(
  312. '签字项标题与源系统一致(不再统一写「手写签名」)',
  313. JSON.stringify(
  314. br.BREAK_ROAD_FLOW_NODES.flatMap((n) =>
  315. n.fields.filter((f) => f.type === 'sign').map((f) => f.label)
  316. )
  317. ) ===
  318. JSON.stringify([
  319. '编制人签字',
  320. '确认人签字',
  321. '安全交底人签字',
  322. '接受交底人签字',
  323. '作业负责人签字',
  324. '所在单位签字',
  325. '消防部门签字',
  326. '安全部门签字',
  327. '审批部门签字',
  328. '签字'
  329. ])
  330. );
  331. check(
  332. '同一环节内不存在重名的签字项(不会出现两个「手写签名」)',
  333. br.BREAK_ROAD_FLOW_NODES.every((n) => {
  334. const labels = n.fields
  335. .filter((f) => f.type === 'sign')
  336. .map((f) => f.label);
  337. return new Set(labels).size === labels.length;
  338. })
  339. );
  340. check(
  341. '「消防、安全部门意见」的两个签字标题互不相同',
  342. nodeOf('fire_safety_opinion').fields[2].label !==
  343. nodeOf('fire_safety_opinion').fields[3].label
  344. );
  345. check(
  346. '意见类环节的取值 key 互不相同(源系统多处复制同一 model,不能互相覆盖)',
  347. new Set(
  348. br.BREAK_ROAD_FLOW_NODES.filter((n) => n.fields[0].type === 'textarea').map(
  349. (n) => n.fields[0].key
  350. )
  351. ).size === 5
  352. );
  353. check(
  354. '审批部门意见:意见 + 签名',
  355. nodeOf('approver_opinion').fields.length === 2 &&
  356. nodeOf('approver_opinion').fields[0].type === 'textarea' &&
  357. nodeOf('approver_opinion').fields[1].type === 'sign'
  358. );
  359. check(
  360. '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
  361. JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) ===
  362. JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
  363. );
  364. check(
  365. '事中记录表 3 列(时间/记录内容/记录人)',
  366. JSON.stringify(colsOf('execute', 'during_record_list')) ===
  367. JSON.stringify(['时间', '记录内容', '记录人'])
  368. );
  369. check(
  370. '完工验收:意见 + 签名',
  371. nodeOf('acceptance').fields.length === 2 &&
  372. nodeOf('acceptance').fields[0].label === '意见' &&
  373. nodeOf('acceptance').fields[1].type === 'sign'
  374. );
  375. check(
  376. '安全措施默认清单 3 条与源系统 formData 逐条一致',
  377. br.DEFAULT_BREAK_ROAD_MEASURES.length === 3 &&
  378. br.DEFAULT_BREAK_ROAD_MEASURES[0] ===
  379. '作业前,指定交通组织方案,并已通知相关部门或单位' &&
  380. br.DEFAULT_BREAK_ROAD_MEASURES[1] ===
  381. '作业前,在断路的路口和相关道路上设置交通警示标志,在作业区域附近设置路栏、道路作业警示灯、寻向标灯交通警示设施' &&
  382. br.DEFAULT_BREAK_ROAD_MEASURES[2] === '夜间作业设置警示灯'
  383. );
  384. check(
  385. '默认行挂在 measure_list 字段上(defaultRows)',
  386. Array.isArray(nodeOf('measure').fields[0].defaultRows) &&
  387. nodeOf('measure').fields[0].defaultRows.length === 3 &&
  388. nodeOf('measure').fields[0].defaultRows[0].measure_content ===
  389. br.DEFAULT_BREAK_ROAD_MEASURES[0]
  390. );
  391. console.log('\n[5] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
  392. const m = br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD_THREETH');
  393. check('按任务定义 key 精确命中', m && m.key === 'measure', {
  394. key: m && m.key
  395. });
  396. check(
  397. '按节点名称命中',
  398. (br.matchBreakRoadFlowNode('现场安全措施确认') || {}).key === 'measure'
  399. );
  400. check(
  401. '按别名命中',
  402. (br.matchBreakRoadFlowNode('危险辨识') || {}).key === 'risk'
  403. );
  404. check(
  405. '按平台自定义 key 反向包含命中',
  406. (br.matchBreakRoadFlowNode('break_road_risk') || {}).key === 'risk'
  407. );
  408. check(
  409. '第七级按 code 命中消防、安全部门意见',
  410. (br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD_SEVENTH') || {}).key ===
  411. 'fire_safety_opinion'
  412. );
  413. check(
  414. '流程定义 key / 一级作业申请 key 不误命中后续环节',
  415. br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD') === null &&
  416. br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD_ONETH') === null
  417. );
  418. check('空标识返回 null', br.matchBreakRoadFlowNode('', null) === null);
  419. console.log('\n[6] 运行时辅助(回显 / payload / 流程变量)');
  420. const measureNode = nodeOf('measure');
  421. const source = {
  422. measure_list: [
  423. { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' }
  424. ],
  425. measure_preparer_sign: '/tmp/a.png'
  426. };
  427. const loaded = bt.loadNodeValues(measureNode, [source]);
  428. check(
  429. '回显:按自身 key 取到已保存内容',
  430. loaded.measure_list.length === 1 &&
  431. loaded.measure_preparer_sign === '/tmp/a.png'
  432. );
  433. check(
  434. '回显:未保存的字段为 undefined',
  435. loaded.measure_confirmer_sign === undefined
  436. );
  437. const payload = bt.buildNodePayload(measureNode, {
  438. measure_list: [{ measure_content: 'x' }],
  439. measure_preparer_sign: '/tmp/a.png'
  440. });
  441. check(
  442. 'payload:保留原始类型(数组不转字符串)',
  443. Array.isArray(payload.measure_list) && payload.measure_list.length === 1
  444. );
  445. check(
  446. 'payload:空值不写入',
  447. !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload)
  448. );
  449. const vars = bt.buildNodeVariables(measureNode, {
  450. measure_list: [{ measure_content: 'x' }],
  451. measure_preparer_sign: '/tmp/a.png'
  452. });
  453. check(
  454. '流程变量:子表转 JSON 字符串、其余转字符串',
  455. typeof vars.measure_list === 'string' &&
  456. vars.measure_preparer_sign === '/tmp/a.png'
  457. );
  458. check(
  459. '回显:多选框的值不会串到风险辨识说明',
  460. bt.loadNodeValues(nodeOf('risk'), [{ risk_result: '9' }]).risk_result_desc ===
  461. undefined
  462. );
  463. const opinionPayload = bt.buildNodePayload(nodeOf('leader_opinion'), {
  464. leader_opinion: '同意',
  465. leader_sign: '/tmp/leader.png'
  466. });
  467. check(
  468. '作业负责人意见:意见 + 签名写入 payload 与流程变量',
  469. opinionPayload.leader_opinion === '同意' &&
  470. opinionPayload.leader_sign === '/tmp/leader.png' &&
  471. bt.buildNodeVariables(nodeOf('leader_opinion'), {
  472. leader_opinion: '同意'
  473. }).leader_opinion === '同意'
  474. );
  475. check(
  476. '消防、安全部门意见:两处意见互不覆盖',
  477. bt.buildNodePayload(nodeOf('fire_safety_opinion'), {
  478. fire_dept_opinion: '消防意见',
  479. safety_dept_opinion: '安全意见'
  480. }).fire_dept_opinion === '消防意见' &&
  481. bt.buildNodePayload(nodeOf('fire_safety_opinion'), {
  482. fire_dept_opinion: '消防意见',
  483. safety_dept_opinion: '安全意见'
  484. }).safety_dept_opinion === '安全意见'
  485. );
  486. check(
  487. '所在单位意见 / 审批部门意见的 payload 互不覆盖',
  488. bt.buildNodePayload(nodeOf('unit_opinion'), { unit_opinion: '单位意见' })
  489. .unit_opinion === '单位意见' &&
  490. bt.buildNodePayload(nodeOf('approver_opinion'), {
  491. approver_opinion: '审批意见'
  492. }).approver_opinion === '审批意见'
  493. );
  494. console.log('\n[7] 组件模板与脚本一致性(模板调用的方法必须已在组件中定义)');
  495. const compiler = require('vue-template-compiler');
  496. const parser = require('@babel/parser');
  497. /**
  498. * 收集组件脚本里已定义的成员名(props / data / computed / methods)
  499. * @param {string} script script 源码
  500. * @returns {Set<string>} 成员名集合
  501. */
  502. function collectComponentMembers(script) {
  503. const names = new Set();
  504. const ast = parser.parse(script, {
  505. sourceType: 'module',
  506. plugins: [
  507. 'optionalChaining',
  508. 'objectRestSpread',
  509. 'nullishCoalescingOperator'
  510. ]
  511. });
  512. let component = null;
  513. for (const node of ast.program.body) {
  514. if (node.type === 'ExportDefaultDeclaration') {
  515. component = node.declaration;
  516. }
  517. }
  518. const collectObjectKeys = (obj) => {
  519. ((obj && obj.properties) || []).forEach((prop) => {
  520. if (!prop.key) {
  521. return;
  522. }
  523. names.add(prop.key.name || prop.key.value);
  524. });
  525. };
  526. ((component && component.properties) || []).forEach((prop) => {
  527. if (!prop.key) {
  528. return;
  529. }
  530. const key = prop.key.name;
  531. const value = prop.value;
  532. if (key === 'props' || key === 'methods' || key === 'computed') {
  533. collectObjectKeys(value);
  534. return;
  535. }
  536. if (key === 'data') {
  537. // data() { return { ... } }
  538. const body = value && value.body;
  539. if (!body) {
  540. return;
  541. }
  542. const ret = body.body.find(
  543. (stmt) => stmt.type === 'ReturnStatement' && stmt.argument
  544. );
  545. if (ret) {
  546. collectObjectKeys(ret.argument);
  547. }
  548. }
  549. });
  550. return names;
  551. }
  552. /**
  553. * 取模板里被调用的成员名
  554. *
  555. * 注意:Vue 2 编译后的 render 用 with(this) 解析标识符,
  556. * 方法调用编译成裸函数名(如 getRows(field)),不带 vm. 前缀,
  557. * 因此这里直接扫描模板表达式,而不是扫描编译产物。
  558. *
  559. * @param {string} template 模板源码
  560. * @returns {Set<string>} 被调用的成员名集合
  561. */
  562. function collectTemplateCalls(template) {
  563. const calls = new Set();
  564. // 1) 插值 {{ expr }}
  565. // 2) 指令 / 事件绑定属性值: :xxx="expr"、v-xxx="expr"、v-xxx:arg="expr"、@xxx="expr"
  566. const attrRe = /(?:[:@][\w.-]+|v-[\w-]+(?::[\w.-]+)?)\s*=\s*"([^"]*)"/g;
  567. const interpRe = /\{\{([\s\S]*?)\}\}/g;
  568. const exprs = [];
  569. let m;
  570. while ((m = attrRe.exec(template)) !== null) {
  571. exprs.push(m[1]);
  572. }
  573. while ((m = interpRe.exec(template)) !== null) {
  574. exprs.push(m[1]);
  575. }
  576. // 全局函数与 Vue 内部函数,不属于组件成员
  577. const globals = new Set([
  578. 'String',
  579. 'Number',
  580. 'Boolean',
  581. 'Array',
  582. 'Object',
  583. 'JSON',
  584. 'Math',
  585. 'Date',
  586. 'parseInt',
  587. 'parseFloat',
  588. 'isNaN',
  589. 'require'
  590. ]);
  591. // 标识符后紧跟 "(" 即为函数调用;排除属性访问(xxx.method())与内部变量
  592. const callRe = /(^|[^.\w$])([A-Za-z_$][\w$]*)\s*\(/g;
  593. exprs.forEach((expr) => {
  594. let hit;
  595. callRe.lastIndex = 0;
  596. while ((hit = callRe.exec(expr)) !== null) {
  597. const name = hit[2];
  598. if (name.startsWith('$') || name.startsWith('_') || globals.has(name)) {
  599. continue;
  600. }
  601. calls.add(name);
  602. }
  603. });
  604. return { calls, errors: compiler.compile(template).errors || [] };
  605. }
  606. const sfcPath = path.join(SRC, 'components/breakRoadFlow.vue');
  607. const sfcSource = fs.readFileSync(sfcPath, 'utf8');
  608. const sfc = compiler.parseComponent(sfcSource);
  609. const templateCheck = collectTemplateCalls(sfc.template.content);
  610. const members = collectComponentMembers(sfc.script.content);
  611. check(
  612. 'breakRoadFlow.vue 模板编译无错误',
  613. templateCheck.errors.length === 0,
  614. templateCheck.errors
  615. );
  616. const missing = [...templateCheck.calls].filter((name) => !members.has(name));
  617. check(
  618. `模板调用的 ${templateCheck.calls.size} 个方法均已定义`,
  619. missing.length === 0,
  620. missing
  621. );
  622. check(
  623. '模板确实调用了 getConfirmColumn / confirmAll(子表「全部确认」按钮)',
  624. templateCheck.calls.has('getConfirmColumn') &&
  625. templateCheck.calls.has('confirmAll')
  626. );
  627. check(
  628. '未识别到环节时展示当前任务名 / key(便于排查流程模型配置)',
  629. /taskLabel/.test(sfcSource)
  630. );
  631. check(
  632. '子表日期列渲染为日期时间选择器(el-date-picker)',
  633. /<el-date-picker[^>]*column\.type === 'date'/.test(sfcSource)
  634. );
  635. check(
  636. '作业实施:事中记录表「时间」列类型为 date',
  637. nodeOf('execute')
  638. .fields.find((f) => f.key === 'during_record_list')
  639. .columns.some((c) => c.key === 'record_time' && c.type === 'date')
  640. );
  641. check(
  642. '组件对外提供 validate / getPayload / getVariables',
  643. /validate\(\)\s*\{/.test(sfc.script.content) &&
  644. /getPayload\(\)\s*\{/.test(sfc.script.content) &&
  645. /getVariables\(\)\s*\{/.test(sfc.script.content)
  646. );
  647. check(
  648. '组件引用的节点 schema 与运行时辅助均来自断路作业自己的文件',
  649. /from '\.\.\/breakRoadFlowNodes'/.test(sfc.script.content) &&
  650. /from '\.\.\/breakRoadFlowTask'/.test(sfc.script.content)
  651. );
  652. // 其余作业类型组件只做只读体检,不参与断言(既有实现不做任何改动)
  653. [
  654. 'confinedSpaceFlow.vue',
  655. 'heightFlow.vue',
  656. 'hoistingFlow.vue',
  657. 'electricityFlow.vue',
  658. 'breakGroundFlow.vue'
  659. ].forEach((name) => {
  660. const otherSfc = compiler.parseComponent(
  661. fs.readFileSync(path.join(SRC, 'components', name), 'utf8')
  662. );
  663. const otherCheck = collectTemplateCalls(otherSfc.template.content);
  664. const otherMembers = collectComponentMembers(otherSfc.script.content);
  665. const otherMissing = [...otherCheck.calls].filter(
  666. (n) => !otherMembers.has(n)
  667. );
  668. console.log(
  669. ' INFO ' +
  670. name +
  671. ' 模板调用体检:缺失 ' +
  672. otherMissing.length +
  673. ' 个' +
  674. (otherMissing.length ? ' -> ' + JSON.stringify(otherMissing) : '')
  675. );
  676. });
  677. console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED'));
  678. process.exit(failed ? 1 : 0);