check-blind-plate-work-flow-nodes.js 27 KB

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