check-dangerous-work-flow-nodes.js 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391
  1. /**
  2. * 危险作业票「后续环节」统一校验
  3. *
  4. * 用法:node scripts/check-dangerous-work-flow-nodes.js
  5. *
  6. * 背景:
  7. * 八类作业票(动火 / 受限空间 / 高处 / 吊装 / 临时用电 / 动土 / 断路 / 盲板抽堵)
  8. * 的「后续环节」此前是同一份实现复制了八遍(8 份 *FlowNodes.js + 8 份 *FlowTask.js
  9. * + 7 份 xxxFlow.vue),现已收敛为:
  10. * - flowRuntime.js 公共运行时(回显 / payload / 流程变量 / 作业票参数)
  11. * - flowMatcher.js 公共环节匹配器(code -> 别名 -> 名称 -> 双向包含)
  12. * - flowFields.js 字段取值 key 工具(纯函数,不依赖接口)
  13. * - 八份 *FlowNodes.js 只描述各自的节点 schema
  14. * - 七份 components/xxxFlow.vue 各作业类型自己的「后续环节」组件(对外接口一致)
  15. * - detail.vue 由模块级常量 FLOW_TYPES 配置表驱动,
  16. * 环节组件由 <component :is="currentFlowType.component"> 渲染
  17. * 本脚本是原来 7 个 check-*-work-flow-nodes.js 的合并版,
  18. * 基线里的全部 314 条断言都保留了下来,并补充了针对新结构的断言。
  19. *
  20. * 校验内容:
  21. * 1) 三个公共模块存在且导出齐全
  22. * 2) 八份节点定义文件各自导出节点表与匹配包装,包装确实委托给 matchFlowNode;
  23. * 节点表的 key / code / name 唯一,code 以该作业类型的 HO_PERMIT_ 前缀开头
  24. * 3) 各作业类型的环节数量与「第 N 级 code / name」与源系统一致
  25. * 4) 各环节的表单设计与源系统 formJson 一致(字段类型 / 列 / 默认清单)
  26. * 5) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
  27. * 6) 运行时辅助(回显 / payload / 流程变量)的取值口径正确
  28. * 7) detail.vue 由 FLOW_TYPES 配置表驱动,七类作业各自的环节组件在位
  29. * 8) 七个环节组件的对外接口一致性与模板/脚本一致性
  30. * 9) 时间类字段一律用日期时间选择器渲染,不能落回输入框
  31. * 10) 作业票内容(基本信息)的手写签名能正常显示
  32. * 11) 作业类型名称匹配:全等优先 + 包含兜底
  33. * 9) 作业票内容(基本信息)的手写签名能正常显示
  34. *
  35. * 源系统依据:
  36. * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
  37. * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=...
  38. * -> 流程定义 HO_PERMIT_FIRE / HO_PERMIT_CONFINED_SPACE / HO_PERMIT_HEIGHT /
  39. * HO_PERMIT_HOISTING / HO_PERMIT_ELECTRICITY / HO_PERMIT_BREAK_GROUND /
  40. * HO_PERMIT_BREAK_ROAD / HO_PERMIT_BLIND_PLATE
  41. */
  42. const fs = require('fs');
  43. const os = require('os');
  44. const path = require('path');
  45. const babel = require('@babel/core');
  46. const Module = require('module');
  47. const compiler = require('vue-template-compiler');
  48. const parser = require('@babel/parser');
  49. const ROOT = path.resolve(__dirname, '..');
  50. const SRC = path.join(
  51. ROOT,
  52. 'src/views/bpm/handleTask/components/dangerousWorkTicket'
  53. );
  54. const OUT = path.join(os.tmpdir(), 'kd-aiot-check-dangerous-work-flow-nodes');
  55. let failed = 0;
  56. let total = 0;
  57. function check(name, cond, extra) {
  58. total++;
  59. if (cond) {
  60. console.log(' PASS ' + name);
  61. } else {
  62. failed++;
  63. console.log(
  64. ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
  65. );
  66. }
  67. }
  68. // ---------------------------------------------------------------------------
  69. // 一、编译 ESM -> CJS,并准备 @/ 桩模块
  70. // ---------------------------------------------------------------------------
  71. // 八个作业类型的节点定义文件 + 三个公共模块
  72. const COMPILE_FILES = [
  73. 'flowFields.js',
  74. 'flowMatcher.js',
  75. 'flowRuntime.js',
  76. 'workTypes.js',
  77. 'templates.js',
  78. 'fireFlowNodes.js',
  79. 'confinedSpaceFlowNodes.js',
  80. 'heightFlowNodes.js',
  81. 'hoistingFlowNodes.js',
  82. 'electricityFlowNodes.js',
  83. 'breakGroundFlowNodes.js',
  84. 'breakRoadFlowNodes.js',
  85. 'blindPlateFlowNodes.js'
  86. ];
  87. fs.mkdirSync(OUT, { recursive: true });
  88. COMPILE_FILES.forEach((file) => {
  89. const code = fs.readFileSync(path.join(SRC, file), 'utf8');
  90. const res = babel.transformSync(code, {
  91. filename: file,
  92. babelrc: false,
  93. configFile: false,
  94. plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
  95. });
  96. fs.writeFileSync(path.join(OUT, file), res.code);
  97. });
  98. // 运行时辅助会引用 @/api/*,这里用桩模块代替,避免校验脚本依赖后端接口
  99. const STUB = path.join(OUT, 'stub.js');
  100. fs.writeFileSync(
  101. STUB,
  102. 'module.exports = {\n' +
  103. ' getTaskListByProcessInstanceId: function () { return Promise.resolve([]); },\n' +
  104. ' getProcessInstance: function () { return Promise.resolve({}); }\n' +
  105. '};\n'
  106. );
  107. const originalResolve = Module._resolveFilename;
  108. Module._resolveFilename = function (request, ...args) {
  109. if (request.startsWith('@/')) {
  110. return STUB;
  111. }
  112. return originalResolve.call(this, request, ...args);
  113. };
  114. const runtime = require(path.join(OUT, 'flowRuntime.js'));
  115. const matcher = require(path.join(OUT, 'flowMatcher.js'));
  116. const fieldUtils = require(path.join(OUT, 'flowFields.js'));
  117. // ---------------------------------------------------------------------------
  118. // 二、八类作业的配置表(数据驱动)
  119. // ---------------------------------------------------------------------------
  120. const TYPES = [
  121. {
  122. key: 'fire',
  123. label: '动火',
  124. module: 'fireFlowNodes',
  125. nodesConst: 'FIRE_FLOW_NODES',
  126. matchFn: 'matchFireFlowNode',
  127. measuresConst: 'DEFAULT_FIRE_MEASURES',
  128. codePrefix: 'HO_PERMIT_FIRE_',
  129. flowCode: 'HO_PERMIT_FIRE',
  130. // 动火沿用自己的实现,不在 detail.vue 的 FLOW_TYPES 配置表内
  131. templateKey: null,
  132. count: 11,
  133. expected: [
  134. ['HO_PERMIT_FIRE_TWO', '风险辨识'],
  135. ['HO_PERMIT_FIRE_THREETH', '气体分析'],
  136. ['HO_PERMIT_FIRE_FOURTH', '现场安全措施落实'],
  137. ['HO_PERMIT_FIRE_FIVETH', '安全交底'],
  138. ['HO_PERMIT_FIRE_SIXTH', '作业负责人意见'],
  139. ['HO_PERMIT_FIRE_SEVENTH', '所在单位意见'],
  140. ['HO_PERMIT_FIRE_EIGHTTH', '安全管理部门意见'],
  141. ['HO_PERMIT_FIRE_NINETH', '动火审批人意见'],
  142. ['HO_PERMIT_FIRE_TENTH', '动火前验票'],
  143. ['HO_PERMIT_FIRE_ELETH', '作业实施'],
  144. ['HO_PERMIT_FIRE_TWETEENTH', '完工验收']
  145. ]
  146. },
  147. {
  148. key: 'confined_space',
  149. label: '受限空间',
  150. module: 'confinedSpaceFlowNodes',
  151. nodesConst: 'CONFINED_SPACE_FLOW_NODES',
  152. matchFn: 'matchConfinedSpaceFlowNode',
  153. measuresConst: 'DEFAULT_CONFINED_SPACE_MEASURES',
  154. codePrefix: 'HO_PERMIT_CONFINED_SPACE_',
  155. flowCode: 'HO_PERMIT_CONFINED_SPACE',
  156. templateKey: 'confined_space',
  157. count: 7,
  158. expected: [
  159. ['HO_PERMIT_CONFINED_SPACE_TWOTH', '风险辨识'],
  160. ['HO_PERMIT_CONFINED_SPACE_THREETH', '气体分析'],
  161. ['HO_PERMIT_CONFINED_SPACE_FOURTH', '现场安全措施确认'],
  162. ['HO_PERMIT_CONFINED_SPACE_FIVETH', '安全交底'],
  163. ['HO_PERMIT_CONFINED_SPACE_SIXTH', '所在单位意见'],
  164. ['HO_PERMIT_CONFINED_SPACE_SEVENTH', '作业实施'],
  165. ['HO_PERMIT_CONFINED_SPACE_EIGHTTH', '完工验收']
  166. ]
  167. },
  168. {
  169. key: 'height',
  170. label: '高处',
  171. module: 'heightFlowNodes',
  172. nodesConst: 'HEIGHT_FLOW_NODES',
  173. matchFn: 'matchHeightFlowNode',
  174. measuresConst: 'DEFAULT_HEIGHT_MEASURES',
  175. codePrefix: 'HO_PERMIT_HEIGHT_',
  176. flowCode: 'HO_PERMIT_HEIGHT',
  177. templateKey: 'height',
  178. count: 9,
  179. expected: [
  180. ['HO_PERMIT_HEIGHT_TWOTH', '风险辨识'],
  181. ['HO_PERMIT_HEIGHT_THREETH', '现场安全措施确认'],
  182. ['HO_PERMIT_HEIGHT_FOURTH', '安全交底'],
  183. ['HO_PERMIT_HEIGHT_FIVETH', '作业负责人意见'],
  184. ['HO_PERMIT_HEIGHT_SIXTH', '所在单位意见'],
  185. ['HO_PERMIT_HEIGHT_SEVENTH', '审核部门意见'],
  186. ['HO_PERMIT_HEIGHT_EIGHTTH', '审批部门意见'],
  187. ['HO_PERMIT_HEIGHT_NINETH', '作业实施'],
  188. ['HO_PERMIT_HEIGHT_TENTH', '完工验收']
  189. ]
  190. },
  191. {
  192. key: 'hoisting',
  193. label: '吊装',
  194. module: 'hoistingFlowNodes',
  195. nodesConst: 'HOISTING_FLOW_NODES',
  196. matchFn: 'matchHoistingFlowNode',
  197. measuresConst: 'DEFAULT_HOISTING_MEASURES',
  198. codePrefix: 'HO_PERMIT_HOISTING_',
  199. flowCode: 'HO_PERMIT_HOISTING',
  200. templateKey: 'hoisting',
  201. count: 9,
  202. expected: [
  203. ['HO_PERMIT_HOISTING_TWOTH', '风险辨识'],
  204. ['HO_PERMIT_HOISTING_THREETH', '现场安全措施确认'],
  205. ['HO_PERMIT_HOISTING_FOURTH', '安全交底'],
  206. ['HO_PERMIT_HOISTING_FIVETH', '作业负责人意见'],
  207. ['HO_PERMIT_HOISTING_SIXTH', '所在单位意见'],
  208. ['HO_PERMIT_HOISTING_SEVENTH', '审核部门意见'],
  209. ['HO_PERMIT_HOISTING_EIGHTTH', '审批部门意见'],
  210. ['HO_PERMIT_HOISTING_NINETH', '作业实施'],
  211. ['HO_PERMIT_HOISTING_TENTH', '完工验收']
  212. ]
  213. },
  214. {
  215. key: 'electricity',
  216. label: '临时用电',
  217. module: 'electricityFlowNodes',
  218. nodesConst: 'ELECTRICITY_FLOW_NODES',
  219. matchFn: 'matchElectricityFlowNode',
  220. measuresConst: 'DEFAULT_ELECTRICITY_MEASURES',
  221. codePrefix: 'HO_PERMIT_ELECTRICITY_',
  222. flowCode: 'HO_PERMIT_ELECTRICITY',
  223. templateKey: 'electricity',
  224. count: 9,
  225. expected: [
  226. ['HO_PERMIT_ELECTRICITY_TWOTH', '风险辨识'],
  227. ['HO_PERMIT_ELECTRICITY_THREETH', '气体分析'],
  228. ['HO_PERMIT_ELECTRICITY_FOURTH', '危害辨识'],
  229. ['HO_PERMIT_ELECTRICITY_FIVETH', '现场安全措施确认'],
  230. ['HO_PERMIT_ELECTRICITY_SIXTH', '安全交底'],
  231. ['HO_PERMIT_ELECTRICITY_SEVENTH', '作业负责人意见'],
  232. ['HO_PERMIT_ELECTRICITY_EIGHTTH', '用电单位意见'],
  233. ['HO_PERMIT_ELECTRICITY_NINETH', '作业实施'],
  234. ['HO_PERMIT_ELECTRICITY_TENTH', '完工验收']
  235. ]
  236. },
  237. {
  238. key: 'break_ground',
  239. label: '动土',
  240. module: 'breakGroundFlowNodes',
  241. nodesConst: 'BREAK_GROUND_FLOW_NODES',
  242. matchFn: 'matchBreakGroundFlowNode',
  243. measuresConst: 'DEFAULT_BREAK_GROUND_MEASURES',
  244. codePrefix: 'HO_PERMIT_BREAK_GROUND_',
  245. flowCode: 'HO_PERMIT_BREAK_GROUND',
  246. templateKey: 'break_ground',
  247. count: 9,
  248. expected: [
  249. ['HO_PERMIT_BREAK_GROUND_TWOTH', '风险辨识'],
  250. ['HO_PERMIT_BREAK_GROUND_THREETH', '现场安全措施确认'],
  251. ['HO_PERMIT_BREAK_GROUND_FOURTH', '安全交底'],
  252. ['HO_PERMIT_BREAK_GROUND_FIVETH', '作业负责人意见'],
  253. ['HO_PERMIT_BREAK_GROUND_SIXTH', '所在单位意见'],
  254. ['HO_PERMIT_BREAK_GROUND_SEVENTH', '有关部门会签意见'],
  255. ['HO_PERMIT_BREAK_GROUND_EIGHTTH', '审批部门意见'],
  256. ['HO_PERMIT_BREAK_GROUND_NINETH', '作业实施'],
  257. ['HO_PERMIT_BREAK_GROUND_TENTH', '完工验收']
  258. ]
  259. },
  260. {
  261. key: 'break_road',
  262. label: '断路',
  263. module: 'breakRoadFlowNodes',
  264. nodesConst: 'BREAK_ROAD_FLOW_NODES',
  265. matchFn: 'matchBreakRoadFlowNode',
  266. measuresConst: 'DEFAULT_BREAK_ROAD_MEASURES',
  267. codePrefix: 'HO_PERMIT_BREAK_ROAD_',
  268. flowCode: 'HO_PERMIT_BREAK_ROAD',
  269. templateKey: 'break_road',
  270. count: 9,
  271. expected: [
  272. ['HO_PERMIT_BREAK_ROAD_TWOTH', '风险辨识'],
  273. ['HO_PERMIT_BREAK_ROAD_THREETH', '现场安全措施确认'],
  274. ['HO_PERMIT_BREAK_ROAD_FOURTH', '安全交底'],
  275. ['HO_PERMIT_BREAK_ROAD_FIVETH', '作业负责人意见'],
  276. ['HO_PERMIT_BREAK_ROAD_SIXTH', '所在单位意见'],
  277. ['HO_PERMIT_BREAK_ROAD_SEVENTH', '消防、安全部门意见'],
  278. ['HO_PERMIT_BREAK_ROAD_EIGHTTH', '审批部门意见'],
  279. ['HO_PERMIT_BREAK_ROAD_NINETH', '作业实施'],
  280. ['HO_PERMIT_BREAK_ROAD_TENTH', '完工验收']
  281. ]
  282. },
  283. {
  284. key: 'blind_plate',
  285. label: '盲板抽堵',
  286. module: 'blindPlateFlowNodes',
  287. nodesConst: 'BLIND_PLATE_FLOW_NODES',
  288. matchFn: 'matchBlindPlateFlowNode',
  289. measuresConst: 'DEFAULT_BLIND_PLATE_MEASURES',
  290. codePrefix: 'HO_PERMIT_BLIND_PLATE_',
  291. flowCode: 'HO_PERMIT_BLIND_PLATE',
  292. templateKey: 'blind_plate',
  293. count: 7,
  294. expected: [
  295. ['HO_PERMIT_BLIND_PLATE_TWOTH', '风险辨识'],
  296. ['HO_PERMIT_BLIND_PLATE_THREETH', '现场安全措施确认'],
  297. ['HO_PERMIT_BLIND_PLATE_FOURTH', '安全交底'],
  298. ['HO_PERMIT_BLIND_PLATE_FIVETH', '作业负责人意见'],
  299. ['HO_PERMIT_BLIND_PLATE_SIXTH', '所在单位意见'],
  300. ['HO_PERMIT_BLIND_PLATE_SEVENTH', '作业实施'],
  301. ['HO_PERMIT_BLIND_PLATE_EIGHTTH', '完工验收']
  302. ]
  303. }
  304. ];
  305. // 载入八份节点定义文件,组装每个作业类型的上下文
  306. TYPES.forEach((type) => {
  307. type.mod = require(path.join(OUT, type.module + '.js'));
  308. type.nodes = type.mod[type.nodesConst];
  309. type.measures = type.mod[type.measuresConst];
  310. type.match = type.mod[type.matchFn];
  311. type.nodeOf = (k) => type.nodes.find((n) => n.key === k);
  312. type.colsOf = (k, fk) => {
  313. const node = type.nodeOf(k);
  314. const field = node.fields.find((f) => f.key === fk);
  315. return (field.columns || []).map((c) => c.label);
  316. };
  317. type.colOf = (k, fk, ck) => {
  318. const node = type.nodeOf(k);
  319. const field = node.fields.find((f) => f.key === fk);
  320. return (field.columns || []).find((c) => c.key === ck);
  321. };
  322. });
  323. // ---------------------------------------------------------------------------
  324. // 三、各作业类型的断言清单(按类型数据驱动)
  325. // ---------------------------------------------------------------------------
  326. /**
  327. * 生成「意见 + 签名」类环节的断言(只校验字段数量与类型)
  328. * @param {Array} list [节点 key, 环节标题] 的列表
  329. */
  330. function opinionChecks(list) {
  331. return list.map((item) => ({
  332. name: item[1] + ':意见 + 签名',
  333. test: (t) => {
  334. const node = t.nodeOf(item[0]);
  335. return (
  336. node.fields.length === 2 &&
  337. node.fields[0].type === 'textarea' &&
  338. node.fields[1].type === 'sign'
  339. );
  340. },
  341. extra: (t) => ({ labels: t.nodeOf(item[0]).fields.map((f) => f.label) })
  342. }));
  343. }
  344. /**
  345. * 生成「意见 + 签名」类环节的断言(同时校验字段标题)
  346. * @param {Array} list [节点 key, 环节标题, 签字标题] 的列表
  347. */
  348. function titledOpinionChecks(list) {
  349. return list.map((item) => ({
  350. name: item[1] + ':意见 + 签名',
  351. test: (t) => {
  352. const node = t.nodeOf(item[0]);
  353. return (
  354. node.fields.length === 2 &&
  355. node.fields[0].type === 'textarea' &&
  356. node.fields[0].label === item[1] &&
  357. node.fields[1].type === 'sign' &&
  358. node.fields[1].label === item[2]
  359. );
  360. },
  361. extra: (t) => ({ labels: t.nodeOf(item[0]).fields.map((f) => f.label) })
  362. }));
  363. }
  364. /**
  365. * 「现场安全措施确认」表名 + 3 列的断言(八个作业类型里除动火外口径一致)
  366. */
  367. const measureTableCheck = {
  368. name: '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)',
  369. test: (t) =>
  370. t.nodeOf('measure').fields[0].label === '安全措施确认表' &&
  371. JSON.stringify(t.colsOf('measure', 'measure_list')) ===
  372. JSON.stringify(['安全措施', '是否确认', '确认人'])
  373. };
  374. /**
  375. * 「现场安全措施确认」编制人签字 + 确认人签字的断言
  376. */
  377. const measureSignCheck = {
  378. name: '现场安全措施确认:编制人签字 + 确认人签字',
  379. test: (t) =>
  380. JSON.stringify(t.nodeOf('measure').fields.map((f) => f.label)) ===
  381. JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字'])
  382. };
  383. /**
  384. * 「默认行挂在 measure_list 字段上(defaultRows)」的断言
  385. * @param {number} count 默认清单条数
  386. */
  387. function defaultRowsCheck(count) {
  388. return {
  389. name: '默认行挂在 measure_list 字段上(defaultRows)',
  390. test: (t) =>
  391. Array.isArray(t.nodeOf('measure').fields[0].defaultRows) &&
  392. t.nodeOf('measure').fields[0].defaultRows.length === count &&
  393. t.nodeOf('measure').fields[0].defaultRows[0].measure_content ===
  394. t.measures[0]
  395. };
  396. }
  397. /**
  398. * 「事中记录表 3 列」的断言
  399. * @param {string[]} labels 列标题
  400. */
  401. function duringRecordCheck(labels) {
  402. return {
  403. name: '事中记录表 3 列(' + labels.join('/') + ')',
  404. test: (t) =>
  405. JSON.stringify(t.colsOf('execute', 'during_record_list')) ===
  406. JSON.stringify(labels)
  407. };
  408. }
  409. // 与源系统 formJson 一致性的断言(逐类型)
  410. const FORM_CHECKS = {
  411. fire: [
  412. {
  413. name: '风险辨识:结果为多选 20 项与源系统完全一致',
  414. test: (t) => {
  415. const f = t.nodeOf('risk').fields;
  416. return (
  417. f[0].type === 'checkbox' &&
  418. f[0].options.length === 20 &&
  419. f[0].options[0].label === '物体打击' &&
  420. f[0].options[19].label === '其它伤害'
  421. );
  422. }
  423. },
  424. {
  425. name: '安全交底:三个签字(安全交底人签字 / 接受交底人签字 / 监护人签字)',
  426. test: (t) =>
  427. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  428. JSON.stringify(['安全交底人签字', '接受交底人签字', '监护人签字'])
  429. },
  430. {
  431. name: '安全措施默认清单 15 条与源系统 formData 逐条一致',
  432. test: (t) =>
  433. t.measures.length === 15 &&
  434. t.measures[0] ===
  435. '动火设备内部构件清洗干净,蒸汽吹扫或水洗、置换合格,达到动火条件' &&
  436. t.measures[14] ===
  437. '其他相关特殊作业已办理相应安全作业票,作业现场四周已设立警戒区'
  438. }
  439. ],
  440. confined_space: [
  441. {
  442. name: '气体分析:表名「气体分析记录表」',
  443. test: (t) => t.nodeOf('gas_analysis').fields[0].label === '气体分析记录表'
  444. },
  445. {
  446. name: '气体分析:6 列且顺序与源系统一致(分析项目/气体名称/合格标准/取样分析时间/分析部位/分析人)',
  447. test: (t) =>
  448. JSON.stringify(t.colsOf('gas_analysis', 'gas_analysis_list')) ===
  449. JSON.stringify([
  450. '分析项目',
  451. '气体名称',
  452. '合格标准',
  453. '取样分析时间',
  454. '分析部位',
  455. '分析人'
  456. ]),
  457. extra: (t) => t.colsOf('gas_analysis', 'gas_analysis_list')
  458. },
  459. {
  460. name: '气体分析:分析项目 3 个选项(有毒有害气体/可燃气体/氧气含量)',
  461. test: (t) =>
  462. JSON.stringify(
  463. t.nodeOf('gas_analysis').fields[0].columns[0].options.map((o) => o.label)
  464. ) === JSON.stringify(['有毒有害气体', '可燃气体', '氧气含量'])
  465. },
  466. measureTableCheck,
  467. measureSignCheck,
  468. {
  469. name: '安全交底:只有 2 个签字(源系统无监护人签字)',
  470. test: (t) =>
  471. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  472. JSON.stringify(['安全交底人签字', '接受交底人签字'])
  473. },
  474. {
  475. name: '所在单位意见:意见 + 签字',
  476. test: (t) => {
  477. const node = t.nodeOf('unit_opinion');
  478. return (
  479. node.fields.length === 2 &&
  480. node.fields[0].type === 'textarea' &&
  481. node.fields[1].type === 'sign'
  482. );
  483. }
  484. },
  485. {
  486. name: '作业实施:实施时间 + 结束时间 + 事中记录表 + 气体分析记录表',
  487. test: (t) =>
  488. JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
  489. JSON.stringify([
  490. '作业实施时间',
  491. '作业结束时间',
  492. '事中记录表',
  493. '气体分析记录表'
  494. ])
  495. },
  496. {
  497. name: '作业实施:气体分析记录表 6 列与源系统一致(时间标签为「气体取样分析时间」)',
  498. test: (t) =>
  499. JSON.stringify(t.colsOf('execute', 'gas_record_list')) ===
  500. JSON.stringify([
  501. '分析项目',
  502. '气体名称',
  503. '合格标准',
  504. '气体取样分析时间',
  505. '分析部位',
  506. '分析人'
  507. ]),
  508. extra: (t) => t.colsOf('execute', 'gas_record_list')
  509. },
  510. duringRecordCheck(['时间', '记录内容', '记录人']),
  511. {
  512. name: '完工验收:意见 + 签字',
  513. test: (t) => {
  514. const node = t.nodeOf('acceptance');
  515. return (
  516. node.fields.length === 2 &&
  517. node.fields[0].label === '意见' &&
  518. node.fields[1].type === 'sign'
  519. );
  520. }
  521. },
  522. {
  523. name: '风险辨识:多选 20 项与源系统完全一致',
  524. test: (t) => {
  525. const f = t.nodeOf('risk').fields;
  526. return (
  527. f[0].options.length === 20 &&
  528. f[0].options[0].label === '物体打击' &&
  529. f[0].options[19].label === '其它伤害'
  530. );
  531. }
  532. },
  533. {
  534. name: '风险辨识:说明字段别名不与多选框标签冲突',
  535. test: (t) => !t.nodeOf('risk').fields[1].alias.includes('风险辨识结果')
  536. },
  537. {
  538. name: '安全措施默认清单 14 条与源系统 formData 逐条一致',
  539. test: (t) =>
  540. t.measures.length === 14 &&
  541. t.measures[0] ===
  542. '盛装过有毒、可燃物料的受限空间,所有与受限空间有联系的阀门、管线已加盲板隔离,并落实盲板责任人,未采用水封或关闭阀门代替盲板' &&
  543. t.measures[3] ===
  544. '转动设备已切断电源,电源开关处已加锁并悬挂“禁止合闸”标志牌' &&
  545. t.measures[13] === '其他相关特殊作业已办理相应安全作业票'
  546. },
  547. defaultRowsCheck(14)
  548. ],
  549. height: [
  550. {
  551. name: '风险辨识:结果为单选(源系统 radio,受限空间为多选)',
  552. test: (t) => {
  553. const f = t.nodeOf('risk').fields;
  554. return (
  555. f[0].type === 'radio' &&
  556. f[0].options.length === 20 &&
  557. f[0].options[0].label === '物体打击' &&
  558. f[0].options[19].label === '其它伤害'
  559. );
  560. }
  561. },
  562. {
  563. name: '风险辨识:说明字段为单行文本(源系统 input)',
  564. test: (t) => t.nodeOf('risk').fields[1].type === 'text'
  565. },
  566. {
  567. name: '风险辨识:说明字段别名不与单选框 key 冲突',
  568. test: (t) =>
  569. !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
  570. !t.nodeOf('risk').fields[1].alias.includes('riskResult')
  571. },
  572. measureTableCheck,
  573. measureSignCheck,
  574. {
  575. name: '安全交底:源系统未配置表单项,按同族流程惯例补齐「安全交底人签字 + 接受交底人签字」',
  576. test: (t) =>
  577. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  578. JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
  579. t.nodeOf('brief').fields.every((f) => f.type === 'sign')
  580. },
  581. ...opinionChecks([
  582. ['leader_opinion', '作业负责人意见'],
  583. ['unit_opinion', '所在单位意见'],
  584. ['audit_opinion', '审核部门意见'],
  585. ['approver_opinion', '审批部门意见']
  586. ]),
  587. {
  588. name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
  589. test: (t) =>
  590. JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
  591. JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
  592. },
  593. duringRecordCheck(['日期时间', '记录内容', '记录人']),
  594. {
  595. name: '完工验收:意见 + 签名',
  596. test: (t) => {
  597. const node = t.nodeOf('acceptance');
  598. return (
  599. node.fields.length === 2 &&
  600. node.fields[0].label === '意见' &&
  601. node.fields[1].type === 'sign'
  602. );
  603. }
  604. },
  605. {
  606. name: '安全措施默认清单 14 条与源系统 formData 逐条一致',
  607. test: (t) =>
  608. t.measures.length === 14 &&
  609. t.measures[0] === '作业人员身体条件符合要求' &&
  610. t.measures[2] ===
  611. '作业人员佩戴符合标准要求的安全帽、安全带,有可能散发有毒气体的场所携带正压式空气呼吸器或面罩备用' &&
  612. t.measures[10] === '30 m 以上高处作业时,作业人员已配备通信、联络工具' &&
  613. t.measures[13] === '其他相关特殊作业已办理相应安全作业票'
  614. },
  615. defaultRowsCheck(14)
  616. ],
  617. hoisting: [
  618. {
  619. name: '风险辨识:结果为单选(源系统 radio,受限空间为多选)',
  620. test: (t) => {
  621. const f = t.nodeOf('risk').fields;
  622. return (
  623. f[0].type === 'radio' &&
  624. f[0].options.length === 20 &&
  625. f[0].options[0].label === '物体打击' &&
  626. f[0].options[19].label === '其它伤害'
  627. );
  628. }
  629. },
  630. {
  631. name: '风险辨识:说明字段为单行文本(源系统 input)',
  632. test: (t) => t.nodeOf('risk').fields[1].type === 'text'
  633. },
  634. {
  635. name: '风险辨识:说明字段别名不与单选框 key 冲突',
  636. test: (t) =>
  637. !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
  638. !t.nodeOf('risk').fields[1].alias.includes('riskResult')
  639. },
  640. measureTableCheck,
  641. measureSignCheck,
  642. {
  643. name: '安全交底:安全交底人签字 + 接受交底人签字',
  644. test: (t) =>
  645. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  646. JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
  647. t.nodeOf('brief').fields.every((f) => f.type === 'sign')
  648. },
  649. ...opinionChecks([
  650. ['leader_opinion', '作业负责人意见'],
  651. ['unit_opinion', '所在单位意见'],
  652. ['audit_opinion', '审核部门意见'],
  653. ['approver_opinion', '审批部门意见']
  654. ]),
  655. {
  656. name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
  657. test: (t) =>
  658. JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
  659. JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
  660. },
  661. duringRecordCheck(['日期时间', '记录内容', '记录人']),
  662. {
  663. name: '完工验收:意见 + 签名',
  664. test: (t) => {
  665. const node = t.nodeOf('acceptance');
  666. return (
  667. node.fields.length === 2 &&
  668. node.fields[0].label === '意见' &&
  669. node.fields[1].type === 'sign'
  670. );
  671. }
  672. },
  673. {
  674. name: '安全措施默认清单 20 条与源系统 formData 逐条一致',
  675. test: (t) =>
  676. JSON.stringify(t.measures) === JSON.stringify(HOISTING_SOURCE_MEASURES),
  677. extra: (t) => ({
  678. len: t.measures.length,
  679. first: t.measures[0],
  680. last: t.measures[t.measures.length - 1]
  681. })
  682. },
  683. defaultRowsCheck(20)
  684. ],
  685. electricity: [
  686. {
  687. name: '气体分析:表名「气体分析记录表」',
  688. test: (t) => t.nodeOf('gas_analysis').fields[0].label === '气体分析记录表'
  689. },
  690. {
  691. name: '气体分析:4 列且顺序与源系统一致(分析时间/分析点/分析结果/分析人)',
  692. test: (t) =>
  693. JSON.stringify(t.colsOf('gas_analysis', 'gas_analysis_list')) ===
  694. JSON.stringify(['分析时间', '分析点', '分析结果', '分析人']),
  695. extra: (t) => t.colsOf('gas_analysis', 'gas_analysis_list')
  696. },
  697. {
  698. name: '气体分析:分析时间为日期类型、其余为多行文本(与源系统一致)',
  699. test: (t) => {
  700. const c = t.nodeOf('gas_analysis').fields[0].columns;
  701. return (
  702. c[0].type === 'date' &&
  703. c[1].type === 'textarea' &&
  704. c[2].type === 'textarea' &&
  705. c[3].type === 'textarea'
  706. );
  707. }
  708. },
  709. {
  710. name: '危害辨识:多选 + 单行文本说明(源系统该节点表单复制自风险辨识)',
  711. test: (t) => {
  712. const f = t.nodeOf('hazard').fields;
  713. return (
  714. f[0].type === 'checkbox' && f[0].options.length === 20 && f[1].type === 'text'
  715. );
  716. }
  717. },
  718. {
  719. name: '风险辨识:多选 20 项 + 单行文本说明(源系统 checkbox + input)',
  720. test: (t) => {
  721. const f = t.nodeOf('risk').fields;
  722. return (
  723. f[0].type === 'checkbox' &&
  724. f[0].options.length === 20 &&
  725. f[0].options[0].label === '物体打击' &&
  726. f[0].options[19].label === '其它伤害' &&
  727. f[1].type === 'text'
  728. );
  729. }
  730. },
  731. {
  732. name: '风险辨识:说明字段别名不与多选框标签冲突',
  733. test: (t) => !t.nodeOf('risk').fields[1].alias.includes('风险辨识结果')
  734. },
  735. measureTableCheck,
  736. measureSignCheck,
  737. {
  738. name: '是否确认选项为 不涉及 / 已确认',
  739. test: (t) =>
  740. JSON.stringify(
  741. t.nodeOf('measure').fields[0].columns[1].options.map((o) => o.label)
  742. ) === JSON.stringify(['不涉及', '已确认'])
  743. },
  744. {
  745. name: '安全交底:2 个签字(安全交底人签字 / 接受交底人签字)',
  746. test: (t) =>
  747. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  748. JSON.stringify(['安全交底人签字', '接受交底人签字'])
  749. },
  750. {
  751. name: '作业负责人意见:意见 + 签字',
  752. test: (t) => {
  753. const node = t.nodeOf('leader_opinion');
  754. return (
  755. node.fields.length === 2 &&
  756. node.fields[0].type === 'textarea' &&
  757. node.fields[1].type === 'sign'
  758. );
  759. }
  760. },
  761. {
  762. name: '用电单位意见:意见 + 签字(不是受限空间的「所在单位意见」)',
  763. test: (t) => {
  764. const node = t.nodeOf('electric_unit_opinion');
  765. return (
  766. node.fields.length === 2 &&
  767. node.fields[0].type === 'textarea' &&
  768. node.fields[1].type === 'sign' &&
  769. !t.nodes.some((n) => n.name === '所在单位意见')
  770. );
  771. }
  772. },
  773. {
  774. name: '作业实施:开始时间 + 结束时间 + 事中记录表 + 可燃气体分析记录表',
  775. test: (t) =>
  776. JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
  777. JSON.stringify([
  778. '作业实施开始时间',
  779. '作业实施结束时间',
  780. '事中记录表',
  781. '可燃气体分析记录表'
  782. ])
  783. },
  784. duringRecordCheck(['时间', '记录内容', '记录人']),
  785. {
  786. name: '可燃气体分析记录表 4 列(分析时间/分析点/分析结果/分析人)',
  787. test: (t) =>
  788. JSON.stringify(t.colsOf('execute', 'gas_record_list')) ===
  789. JSON.stringify(['分析时间', '分析点', '分析结果', '分析人'])
  790. },
  791. {
  792. name: '完工验收:意见 + 签字',
  793. test: (t) => {
  794. const node = t.nodeOf('acceptance');
  795. return (
  796. node.fields.length === 2 &&
  797. node.fields[0].label === '意见' &&
  798. node.fields[1].type === 'sign'
  799. );
  800. }
  801. },
  802. {
  803. name: '安全措施默认清单 13 条与源系统 formData 逐条一致',
  804. test: (t) =>
  805. t.measures.length === 13 &&
  806. t.measures[0] === '作业人员持有电工作业操作证' &&
  807. t.measures[2] === '上级开关已断电、加锁,并挂安全警示标牌' &&
  808. t.measures[9] ===
  809. '临时用电设施已装配漏电保护器,移动工具、手持工具已采取防漏电的安全措施(一机一闸一保护)' &&
  810. t.measures[12] === '作业场所已进行气体检测且符合作业安全要求'
  811. },
  812. defaultRowsCheck(13),
  813. // 风险辨识 / 危害辨识 取值 key 不冲突
  814. {
  815. name: '两个环节的字段 key 无交集',
  816. test: (t) => {
  817. const riskKeys = t.nodeOf('risk').fields.map((f) => f.key);
  818. const hazardKeys = t.nodeOf('hazard').fields.map((f) => f.key);
  819. return riskKeys.every((k) => !hazardKeys.includes(k));
  820. },
  821. extra: (t) => ({
  822. riskKeys: t.nodeOf('risk').fields.map((f) => f.key),
  823. hazardKeys: t.nodeOf('hazard').fields.map((f) => f.key)
  824. })
  825. },
  826. {
  827. name: '危害辨识使用 hazard_ 前缀',
  828. test: (t) =>
  829. t.nodeOf('hazard')
  830. .fields.map((f) => f.key)
  831. .every((k) => k.startsWith('hazard_')),
  832. extra: (t) => t.nodeOf('hazard').fields.map((f) => f.key)
  833. },
  834. {
  835. name: '危害辨识的别名不会命中风险辨识的取值 key',
  836. test: (t) => {
  837. const hazardAlias = t
  838. .nodeOf('hazard')
  839. .fields.flatMap((f) => f.alias || []);
  840. return (
  841. !hazardAlias.includes('risk_result') &&
  842. !hazardAlias.includes('risk_result_desc')
  843. );
  844. }
  845. }
  846. ],
  847. break_ground: [
  848. {
  849. name: '风险辨识:结果为多选 20 项(源系统 checkbox)',
  850. test: (t) => {
  851. const f = t.nodeOf('risk').fields;
  852. return (
  853. f[0].type === 'checkbox' &&
  854. f[0].options.length === 20 &&
  855. f[0].options[0].label === '物体打击' &&
  856. f[0].options[19].label === '其它伤害'
  857. );
  858. }
  859. },
  860. {
  861. name: '风险辨识:说明字段为单行文本(源系统 input)',
  862. test: (t) => t.nodeOf('risk').fields[1].type === 'text'
  863. },
  864. {
  865. name: '风险辨识:说明字段别名不与多选框 key 冲突',
  866. test: (t) =>
  867. !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
  868. !t.nodeOf('risk').fields[1].alias.includes('riskResult')
  869. },
  870. measureTableCheck,
  871. measureSignCheck,
  872. {
  873. name: '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
  874. test: (t) =>
  875. t
  876. .colOf('measure', 'measure_list', 'measure_confirm')
  877. .options.map((o) => o.label)
  878. .join(',') === '不涉及,已确认'
  879. },
  880. {
  881. name: '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
  882. test: (t) =>
  883. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  884. JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
  885. t.nodeOf('brief').fields.every((f) => f.type === 'sign')
  886. },
  887. ...opinionChecks([
  888. ['leader_opinion', '作业负责人意见'],
  889. ['unit_opinion', '所在单位意见'],
  890. ['countersign_opinion', '有关部门会签意见'],
  891. ['approver_opinion', '审批部门意见']
  892. ]),
  893. {
  894. name: '意见类环节的取值 key 互不相同(源系统四处复制同一 model,不能互相覆盖)',
  895. test: (t) =>
  896. new Set(
  897. t.nodes
  898. .filter((n) => n.fields[0].type === 'textarea')
  899. .map((n) => n.fields[0].key)
  900. ).size === 5
  901. },
  902. {
  903. name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
  904. test: (t) =>
  905. JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
  906. JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
  907. },
  908. duringRecordCheck(['时间', '记录内容', '记录人']),
  909. {
  910. name: '完工验收:意见 + 签名',
  911. test: (t) => {
  912. const node = t.nodeOf('acceptance');
  913. return (
  914. node.fields.length === 2 &&
  915. node.fields[0].label === '意见' &&
  916. node.fields[1].type === 'sign'
  917. );
  918. }
  919. },
  920. {
  921. name: '安全措施默认清单 10 条与源系统 formData 逐条一致',
  922. test: (t) =>
  923. t.measures.length === 10 &&
  924. t.measures[0] ===
  925. '地下电力电缆、通信电(光)缆、局域网络电(光)缆已确认,保护措施已落实' &&
  926. t.measures[4] === '已进行放坡处理和固壁支撑' &&
  927. t.measures[6] ===
  928. '现场夜间有充足照明:A.36 V.24V.12 V 防水型灯:B.36V、24 V.12 V 防爆型灯' &&
  929. t.measures[8] ===
  930. '易燃易爆、有毒气体存在的场所动土深度超过1.2m,已按照受限空间作业要求采取了措施' &&
  931. t.measures[9] === '其他相关特殊作业已办理相应安全作业票'
  932. },
  933. defaultRowsCheck(10)
  934. ],
  935. break_road: [
  936. {
  937. name: '风险辨识:结果为多选 20 项(源系统 checkbox)',
  938. test: (t) => {
  939. const f = t.nodeOf('risk').fields;
  940. return (
  941. f[0].type === 'checkbox' &&
  942. f[0].options.length === 20 &&
  943. f[0].options[0].label === '物体打击' &&
  944. f[0].options[19].label === '其它伤害'
  945. );
  946. }
  947. },
  948. {
  949. name: '风险辨识:说明字段为多行文本(源系统 textarea)',
  950. test: (t) => t.nodeOf('risk').fields[1].type === 'textarea'
  951. },
  952. {
  953. name: '风险辨识:说明字段别名不与多选框 key 冲突',
  954. test: (t) =>
  955. !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
  956. !t.nodeOf('risk').fields[1].alias.includes('riskResult')
  957. },
  958. measureTableCheck,
  959. measureSignCheck,
  960. {
  961. name: '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
  962. test: (t) =>
  963. t
  964. .colOf('measure', 'measure_list', 'measure_confirm')
  965. .options.map((o) => o.label)
  966. .join(',') === '不涉及,已确认'
  967. },
  968. {
  969. name: '现场安全措施确认:确认人列为单行文本(源系统 input)',
  970. test: (t) => t.colOf('measure', 'measure_list', 'measure_confirmer').type === 'text'
  971. },
  972. {
  973. name: '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
  974. test: (t) =>
  975. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  976. JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
  977. t.nodeOf('brief').fields.every((f) => f.type === 'sign')
  978. },
  979. ...opinionChecks([
  980. ['leader_opinion', '作业负责人意见'],
  981. ['unit_opinion', '所在单位意见']
  982. ]),
  983. {
  984. name: '消防、安全部门意见:两个意见框 + 两个签字(消防部门 / 安全部门)',
  985. test: (t) => {
  986. const node = t.nodeOf('fire_safety_opinion');
  987. return (
  988. JSON.stringify(node.fields.map((f) => f.label)) ===
  989. JSON.stringify([
  990. '消防部门意见',
  991. '安全部门意见',
  992. '消防部门签字',
  993. '安全部门签字'
  994. ]) &&
  995. node.fields[0].type === 'textarea' &&
  996. node.fields[1].type === 'textarea' &&
  997. node.fields[2].type === 'sign' &&
  998. node.fields[3].type === 'sign'
  999. );
  1000. }
  1001. },
  1002. {
  1003. name: '签字项标题与源系统一致(不再统一写「手写签名」)',
  1004. test: (t) =>
  1005. JSON.stringify(
  1006. t.nodes.flatMap((n) =>
  1007. n.fields.filter((f) => f.type === 'sign').map((f) => f.label)
  1008. )
  1009. ) ===
  1010. JSON.stringify([
  1011. '编制人签字',
  1012. '确认人签字',
  1013. '安全交底人签字',
  1014. '接受交底人签字',
  1015. '作业负责人签字',
  1016. '所在单位签字',
  1017. '消防部门签字',
  1018. '安全部门签字',
  1019. '审批部门签字',
  1020. '签字'
  1021. ])
  1022. },
  1023. {
  1024. name: '同一环节内不存在重名的签字项(不会出现两个「手写签名」)',
  1025. test: (t) =>
  1026. t.nodes.every((n) => {
  1027. const labels = n.fields
  1028. .filter((f) => f.type === 'sign')
  1029. .map((f) => f.label);
  1030. return new Set(labels).size === labels.length;
  1031. })
  1032. },
  1033. {
  1034. name: '「消防、安全部门意见」的两个签字标题互不相同',
  1035. test: (t) =>
  1036. t.nodeOf('fire_safety_opinion').fields[2].label !==
  1037. t.nodeOf('fire_safety_opinion').fields[3].label
  1038. },
  1039. {
  1040. name: '意见类环节的取值 key 互不相同(源系统多处复制同一 model,不能互相覆盖)',
  1041. test: (t) =>
  1042. new Set(
  1043. t.nodes
  1044. .filter((n) => n.fields[0].type === 'textarea')
  1045. .map((n) => n.fields[0].key)
  1046. ).size === 5
  1047. },
  1048. {
  1049. name: '审批部门意见:意见 + 签名',
  1050. test: (t) => {
  1051. const node = t.nodeOf('approver_opinion');
  1052. return (
  1053. node.fields.length === 2 &&
  1054. node.fields[0].type === 'textarea' &&
  1055. node.fields[1].type === 'sign'
  1056. );
  1057. }
  1058. },
  1059. {
  1060. name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
  1061. test: (t) =>
  1062. JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
  1063. JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
  1064. },
  1065. duringRecordCheck(['时间', '记录内容', '记录人']),
  1066. {
  1067. name: '完工验收:意见 + 签名',
  1068. test: (t) => {
  1069. const node = t.nodeOf('acceptance');
  1070. return (
  1071. node.fields.length === 2 &&
  1072. node.fields[0].label === '意见' &&
  1073. node.fields[1].type === 'sign'
  1074. );
  1075. }
  1076. },
  1077. {
  1078. name: '安全措施默认清单 3 条与源系统 formData 逐条一致',
  1079. test: (t) =>
  1080. t.measures.length === 3 &&
  1081. t.measures[0] === '作业前,指定交通组织方案,并已通知相关部门或单位' &&
  1082. t.measures[1] ===
  1083. '作业前,在断路的路口和相关道路上设置交通警示标志,在作业区域附近设置路栏、道路作业警示灯、寻向标灯交通警示设施' &&
  1084. t.measures[2] === '夜间作业设置警示灯'
  1085. },
  1086. defaultRowsCheck(3)
  1087. ],
  1088. blind_plate: [
  1089. {
  1090. name: '风险辨识:结果为多选 20 项(源系统 checkbox)',
  1091. test: (t) => {
  1092. const f = t.nodeOf('risk').fields;
  1093. return (
  1094. f[0].type === 'checkbox' &&
  1095. f[0].options.length === 20 &&
  1096. f[0].options[0].label === '物体打击' &&
  1097. f[0].options[19].label === '其它伤害'
  1098. );
  1099. }
  1100. },
  1101. {
  1102. name: '风险辨识:说明字段为单行文本(源系统 input)',
  1103. test: (t) => t.nodeOf('risk').fields[1].type === 'text'
  1104. },
  1105. {
  1106. name: '风险辨识:说明字段别名不与多选框 key 冲突',
  1107. test: (t) =>
  1108. !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
  1109. !t.nodeOf('risk').fields[1].alias.includes('riskResult')
  1110. },
  1111. measureTableCheck,
  1112. measureSignCheck,
  1113. {
  1114. name: '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
  1115. test: (t) =>
  1116. t
  1117. .colOf('measure', 'measure_list', 'measure_confirm')
  1118. .options.map((o) => o.label)
  1119. .join(',') === '不涉及,已确认'
  1120. },
  1121. {
  1122. name: '现场安全措施确认:安全措施列为多行文本(源系统 textarea)',
  1123. test: (t) => t.colOf('measure', 'measure_list', 'measure_content').type === 'textarea'
  1124. },
  1125. {
  1126. name: '现场安全措施确认:确认人列为单行文本(源系统 input)',
  1127. test: (t) => t.colOf('measure', 'measure_list', 'measure_confirmer').type === 'text'
  1128. },
  1129. {
  1130. name: '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
  1131. test: (t) =>
  1132. JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
  1133. JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
  1134. t.nodeOf('brief').fields.every((f) => f.type === 'sign')
  1135. },
  1136. ...titledOpinionChecks([
  1137. ['leader_opinion', '作业负责人意见', '作业负责人签字'],
  1138. ['unit_opinion', '所在单位意见', '所在单位签字']
  1139. ]),
  1140. {
  1141. name: '签字项标题能分辨出是谁签的(同一环节内不重名)',
  1142. test: (t) =>
  1143. t.nodes.every((n) => {
  1144. const labels = n.fields
  1145. .filter((f) => f.type === 'sign')
  1146. .map((f) => f.label);
  1147. return new Set(labels).size === labels.length;
  1148. })
  1149. },
  1150. {
  1151. name: '意见类环节的取值 key 互不相同(源系统多处复制同一 model,不能互相覆盖)',
  1152. test: (t) =>
  1153. new Set(
  1154. t.nodes
  1155. .filter((n) => n.fields[0].type === 'textarea')
  1156. .map((n) => n.fields[0].key)
  1157. ).size === 3
  1158. },
  1159. {
  1160. name: '作业实施:实施开始时间 + 事中记录表(源系统只有开始时间,没有结束时间)',
  1161. test: (t) =>
  1162. JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
  1163. JSON.stringify(['作业实施开始时间', '事中记录表'])
  1164. },
  1165. duringRecordCheck(['时间', '记录内容', '记录人']),
  1166. {
  1167. name: '事中记录表「时间」列为日期时间(源系统 datePicker)',
  1168. test: (t) =>
  1169. t
  1170. .nodeOf('execute')
  1171. .fields.find((f) => f.key === 'during_record_list')
  1172. .columns.some((c) => c.key === 'record_time' && c.type === 'date')
  1173. },
  1174. {
  1175. name: '完工验收:意见 + 签名',
  1176. test: (t) => {
  1177. const node = t.nodeOf('acceptance');
  1178. return (
  1179. node.fields.length === 2 &&
  1180. node.fields[0].label === '意见' &&
  1181. node.fields[1].type === 'sign'
  1182. );
  1183. }
  1184. },
  1185. {
  1186. name: '安全措施默认清单 10 条与源系统 formData 逐条一致',
  1187. test: (t) =>
  1188. t.measures.length === 10 &&
  1189. t.measures[0] === '在管道、设备上作业时,降低系统压力,作业点应为常压或微正压' &&
  1190. t.measures[1] ===
  1191. '在有毒介质的管道、设备上作业时,尽可能降低系统压力,作业点应为常压' &&
  1192. t.measures[2] ===
  1193. '在有毒介质的管道、设备上作业时,作业人员穿戴适合的防护用具' &&
  1194. t.measures[3] ===
  1195. '易燃易爆场所,作业人员穿防静电工作服、工作鞋;作业时使用防爆灯具和防爆工具' &&
  1196. t.measures[4] === '易燃易爆场所,距作业地点30m内无其他动火作业' &&
  1197. t.measures[5] ===
  1198. '在强腐蚀性介质的管道、设备上作业时,作业人员已采取防止酸碱灼伤的措施' &&
  1199. t.measures[6] ===
  1200. '介质温度较高、可能造成烫伤的情况下,作业人员已采取防烫措施' &&
  1201. t.measures[7] === '同一管道上不同时进行两处以上的盲板抽堵作业' &&
  1202. t.measures[8] === '其它相关特殊作业已办理相应安全作业票' &&
  1203. t.measures[9] === '作业现场四周已设警戒区'
  1204. },
  1205. defaultRowsCheck(10)
  1206. ]
  1207. };
  1208. // 环节匹配的断言(逐类型)
  1209. const MATCH_CHECKS = {
  1210. fire: [
  1211. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_FIRE_THREETH', key: 'gas_analysis' },
  1212. { name: '按节点名称命中', code: '现场安全措施落实', key: 'measure' },
  1213. { name: '按别名命中', code: '气体检测', key: 'gas_analysis' },
  1214. { name: '按平台自定义 key 反向包含命中', code: 'fire_gas_analysis', key: 'gas_analysis' },
  1215. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_FIRE', 'HO_PERMIT_FIRE_ONETH'] },
  1216. { name: '空标识返回 null', empty: true }
  1217. ],
  1218. confined_space: [
  1219. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_CONFINED_SPACE_THREETH', key: 'gas_analysis' },
  1220. { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
  1221. { name: '按别名命中', code: '气体检测', key: 'gas_analysis' },
  1222. { name: '按平台自定义 key 反向包含命中', code: 'cs_gas_analysis', key: 'gas_analysis' },
  1223. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_CONFINED_SPACE', 'HO_PERMIT_CONFINED_SPACE_ONETH'] },
  1224. { name: '空标识返回 null', empty: true }
  1225. ],
  1226. height: [
  1227. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_HEIGHT_THREETH', key: 'measure' },
  1228. { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
  1229. { name: '按别名命中', code: '危险辨识', key: 'risk' },
  1230. { name: '按平台自定义 key 反向包含命中', code: 'height_risk', key: 'risk' },
  1231. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_HEIGHT', 'HO_PERMIT_HEIGHT_ONETH'] },
  1232. { name: '空标识返回 null', empty: true }
  1233. ],
  1234. hoisting: [
  1235. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_HOISTING_THREETH', key: 'measure' },
  1236. { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
  1237. { name: '按别名命中', code: '危险辨识', key: 'risk' },
  1238. { name: '按平台自定义 key 反向包含命中', code: 'hoisting_risk', key: 'risk' },
  1239. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_HOISTING', 'HO_PERMIT_HOISTING_ONETH'] },
  1240. { name: '空标识返回 null', empty: true }
  1241. ],
  1242. electricity: [
  1243. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_ELECTRICITY_THREETH', key: 'gas_analysis' },
  1244. { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
  1245. { name: '按别名命中', code: '气体检测', key: 'gas_analysis' },
  1246. { name: '按平台自定义 key 反向包含命中', code: 'el_gas_analysis', key: 'gas_analysis' },
  1247. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_ELECTRICITY', 'HO_PERMIT_ELECTRICITY_ONETH'] },
  1248. { name: '空标识返回 null', empty: true },
  1249. { name: '「危害辨识」不会被「风险辨识」抢走', code: '危害辨识', key: 'hazard' },
  1250. { name: '「危险辨识」归到风险辨识节点', code: '危险辨识', key: 'risk' }
  1251. ],
  1252. break_ground: [
  1253. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_BREAK_GROUND_THREETH', key: 'measure' },
  1254. { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
  1255. { name: '按别名命中', code: '危险辨识', key: 'risk' },
  1256. { name: '按平台自定义 key 反向包含命中', code: 'break_ground_risk', key: 'risk' },
  1257. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_BREAK_GROUND', 'HO_PERMIT_BREAK_GROUND_ONETH'] },
  1258. { name: '空标识返回 null', empty: true }
  1259. ],
  1260. break_road: [
  1261. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_BREAK_ROAD_THREETH', key: 'measure' },
  1262. { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
  1263. { name: '按别名命中', code: '危险辨识', key: 'risk' },
  1264. { name: '按平台自定义 key 反向包含命中', code: 'break_road_risk', key: 'risk' },
  1265. { name: '第七级按 code 命中消防、安全部门意见', code: 'HO_PERMIT_BREAK_ROAD_SEVENTH', key: 'fire_safety_opinion' },
  1266. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_BREAK_ROAD', 'HO_PERMIT_BREAK_ROAD_ONETH'] },
  1267. { name: '空标识返回 null', empty: true }
  1268. ],
  1269. blind_plate: [
  1270. { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_BLIND_PLATE_THREETH', key: 'measure' },
  1271. { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
  1272. { name: '按别名命中', code: '危险辨识', key: 'risk' },
  1273. { name: '按平台自定义 key 反向包含命中', code: 'blind_plate_risk', key: 'risk' },
  1274. { name: '第五级按 code 命中作业负责人意见', code: 'HO_PERMIT_BLIND_PLATE_FIVETH', key: 'leader_opinion' },
  1275. { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_BLIND_PLATE', 'HO_PERMIT_BLIND_PLATE_ONETH'] },
  1276. { name: '空标识返回 null', empty: true }
  1277. ]
  1278. };
  1279. // 运行时辅助(回显 / payload / 流程变量)的断言(逐类型)
  1280. const RUNTIME_CHECKS = {
  1281. // 八类作业通用的 5 条
  1282. common: [
  1283. {
  1284. name: '回显:按自身 key 取到已保存内容',
  1285. test: (t) => {
  1286. const loaded = runtime.loadNodeValues(t.nodeOf('measure'), [
  1287. RUNTIME_SOURCE
  1288. ]);
  1289. return (
  1290. loaded.measure_list.length === 1 &&
  1291. loaded.measure_preparer_sign === '/tmp/a.png'
  1292. );
  1293. }
  1294. },
  1295. {
  1296. name: '回显:未保存的字段为 undefined',
  1297. test: (t) =>
  1298. runtime.loadNodeValues(t.nodeOf('measure'), [RUNTIME_SOURCE])
  1299. .measure_confirmer_sign === undefined
  1300. },
  1301. {
  1302. name: 'payload:保留原始类型(数组不转字符串)',
  1303. test: (t) => {
  1304. const payload = runtime.buildNodePayload(t.nodeOf('measure'), {
  1305. measure_list: [{ measure_content: 'x' }],
  1306. measure_preparer_sign: '/tmp/a.png'
  1307. });
  1308. return Array.isArray(payload.measure_list) && payload.measure_list.length === 1;
  1309. }
  1310. },
  1311. {
  1312. name: 'payload:空值不写入',
  1313. test: (t) => {
  1314. const payload = runtime.buildNodePayload(t.nodeOf('measure'), {
  1315. measure_list: [{ measure_content: 'x' }],
  1316. measure_preparer_sign: '/tmp/a.png'
  1317. });
  1318. return (
  1319. !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload)
  1320. );
  1321. }
  1322. },
  1323. {
  1324. name: '流程变量:子表转 JSON 字符串、其余转字符串',
  1325. test: (t) => {
  1326. const vars = runtime.buildNodeVariables(t.nodeOf('measure'), {
  1327. measure_list: [{ measure_content: 'x' }],
  1328. measure_preparer_sign: '/tmp/a.png'
  1329. });
  1330. return (
  1331. typeof vars.measure_list === 'string' &&
  1332. vars.measure_preparer_sign === '/tmp/a.png'
  1333. );
  1334. }
  1335. }
  1336. ],
  1337. fire: [
  1338. {
  1339. name: '流程变量:多选值用逗号连接',
  1340. test: (t) => {
  1341. const vars = runtime.buildNodeVariables(t.nodeOf('risk'), {
  1342. risk_result: ['5', '8'],
  1343. risk_result_desc: '触电、火灾'
  1344. });
  1345. return (
  1346. vars.risk_result === '5,8' && vars.risk_result_desc === '触电、火灾'
  1347. );
  1348. }
  1349. }
  1350. ],
  1351. confined_space: [],
  1352. height: [
  1353. {
  1354. name: '回显:单选框的值不会串到风险辨识说明',
  1355. test: (t) =>
  1356. runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
  1357. .risk_result_desc === undefined
  1358. },
  1359. {
  1360. name: '安全交底:两个签字写入 payload 与流程变量',
  1361. test: (t) => {
  1362. const briefPayload = runtime.buildNodePayload(t.nodeOf('brief'), {
  1363. brief_sign: '/tmp/briefer.png',
  1364. brief_receiver_sign: '/tmp/receiver.png'
  1365. });
  1366. return (
  1367. briefPayload.brief_sign === '/tmp/briefer.png' &&
  1368. briefPayload.brief_receiver_sign === '/tmp/receiver.png' &&
  1369. runtime.buildNodeVariables(t.nodeOf('brief'), {
  1370. brief_sign: '/tmp/briefer.png'
  1371. }).brief_sign === '/tmp/briefer.png'
  1372. );
  1373. }
  1374. }
  1375. ],
  1376. hoisting: [
  1377. {
  1378. name: '回显:单选框的值不会串到风险辨识说明',
  1379. test: (t) =>
  1380. runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
  1381. .risk_result_desc === undefined
  1382. },
  1383. {
  1384. name: '安全交底:两个签字写入 payload 与流程变量',
  1385. test: (t) => {
  1386. const briefPayload = runtime.buildNodePayload(t.nodeOf('brief'), {
  1387. brief_sign: '/tmp/briefer.png',
  1388. brief_receiver_sign: '/tmp/receiver.png'
  1389. });
  1390. return (
  1391. briefPayload.brief_sign === '/tmp/briefer.png' &&
  1392. briefPayload.brief_receiver_sign === '/tmp/receiver.png' &&
  1393. runtime.buildNodeVariables(t.nodeOf('brief'), {
  1394. brief_sign: '/tmp/briefer.png'
  1395. }).brief_sign === '/tmp/briefer.png'
  1396. );
  1397. }
  1398. }
  1399. ],
  1400. electricity: [
  1401. {
  1402. name: '流程变量:多选值用逗号连接',
  1403. test: (t) => {
  1404. const riskVars = runtime.buildNodeVariables(t.nodeOf('risk'), {
  1405. risk_result: ['5', '8'],
  1406. risk_result_desc: '触电、火灾'
  1407. });
  1408. return (
  1409. riskVars.risk_result === '5,8' &&
  1410. riskVars.risk_result_desc === '触电、火灾'
  1411. );
  1412. }
  1413. },
  1414. {
  1415. name: '流程变量:风险辨识与危害辨识各自独立',
  1416. test: (t) => {
  1417. const riskVars = runtime.buildNodeVariables(t.nodeOf('risk'), {
  1418. risk_result: ['5', '8'],
  1419. risk_result_desc: '触电、火灾'
  1420. });
  1421. const hazardVars = runtime.buildNodeVariables(t.nodeOf('hazard'), {
  1422. hazard_result: ['5'],
  1423. hazard_result_desc: '触电'
  1424. });
  1425. return (
  1426. riskVars.risk_result === '5,8' && hazardVars.hazard_result === '5'
  1427. );
  1428. }
  1429. }
  1430. ],
  1431. break_ground: [
  1432. {
  1433. name: '回显:多选框的值不会串到风险辨识说明',
  1434. test: (t) =>
  1435. runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
  1436. .risk_result_desc === undefined
  1437. },
  1438. {
  1439. name: '作业负责人意见:意见 + 签名写入 payload 与流程变量',
  1440. test: (t) => {
  1441. const opinionPayload = runtime.buildNodePayload(
  1442. t.nodeOf('leader_opinion'),
  1443. { leader_opinion: '同意', leader_sign: '/tmp/leader.png' }
  1444. );
  1445. return (
  1446. opinionPayload.leader_opinion === '同意' &&
  1447. opinionPayload.leader_sign === '/tmp/leader.png' &&
  1448. runtime.buildNodeVariables(t.nodeOf('leader_opinion'), {
  1449. leader_opinion: '同意'
  1450. }).leader_opinion === '同意'
  1451. );
  1452. }
  1453. },
  1454. {
  1455. name: '四个意见类环节的 payload 互不覆盖',
  1456. test: (t) =>
  1457. runtime.buildNodePayload(t.nodeOf('unit_opinion'), {
  1458. unit_opinion: '单位意见'
  1459. }).unit_opinion === '单位意见' &&
  1460. runtime.buildNodePayload(t.nodeOf('approver_opinion'), {
  1461. approver_opinion: '审批意见'
  1462. }).approver_opinion === '审批意见'
  1463. }
  1464. ],
  1465. break_road: [
  1466. {
  1467. name: '回显:多选框的值不会串到风险辨识说明',
  1468. test: (t) =>
  1469. runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
  1470. .risk_result_desc === undefined
  1471. },
  1472. {
  1473. name: '作业负责人意见:意见 + 签名写入 payload 与流程变量',
  1474. test: (t) => {
  1475. const opinionPayload = runtime.buildNodePayload(
  1476. t.nodeOf('leader_opinion'),
  1477. { leader_opinion: '同意', leader_sign: '/tmp/leader.png' }
  1478. );
  1479. return (
  1480. opinionPayload.leader_opinion === '同意' &&
  1481. opinionPayload.leader_sign === '/tmp/leader.png' &&
  1482. runtime.buildNodeVariables(t.nodeOf('leader_opinion'), {
  1483. leader_opinion: '同意'
  1484. }).leader_opinion === '同意'
  1485. );
  1486. }
  1487. },
  1488. {
  1489. name: '消防、安全部门意见:两处意见互不覆盖',
  1490. test: (t) => {
  1491. const payload = runtime.buildNodePayload(
  1492. t.nodeOf('fire_safety_opinion'),
  1493. {
  1494. fire_dept_opinion: '消防意见',
  1495. safety_dept_opinion: '安全意见'
  1496. }
  1497. );
  1498. return (
  1499. payload.fire_dept_opinion === '消防意见' &&
  1500. payload.safety_dept_opinion === '安全意见'
  1501. );
  1502. }
  1503. },
  1504. {
  1505. name: '所在单位意见 / 审批部门意见的 payload 互不覆盖',
  1506. test: (t) =>
  1507. runtime.buildNodePayload(t.nodeOf('unit_opinion'), {
  1508. unit_opinion: '单位意见'
  1509. }).unit_opinion === '单位意见' &&
  1510. runtime.buildNodePayload(t.nodeOf('approver_opinion'), {
  1511. approver_opinion: '审批意见'
  1512. }).approver_opinion === '审批意见'
  1513. }
  1514. ],
  1515. blind_plate: [
  1516. {
  1517. name: '回显:多选框的值不会串到风险辨识说明',
  1518. test: (t) =>
  1519. runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
  1520. .risk_result_desc === undefined
  1521. },
  1522. {
  1523. name: '作业负责人意见:意见 + 签名写入 payload 与流程变量',
  1524. test: (t) => {
  1525. const opinionPayload = runtime.buildNodePayload(
  1526. t.nodeOf('leader_opinion'),
  1527. { leader_opinion: '同意', leader_sign: '/tmp/leader.png' }
  1528. );
  1529. return (
  1530. opinionPayload.leader_opinion === '同意' &&
  1531. opinionPayload.leader_sign === '/tmp/leader.png' &&
  1532. runtime.buildNodeVariables(t.nodeOf('leader_opinion'), {
  1533. leader_opinion: '同意'
  1534. }).leader_opinion === '同意'
  1535. );
  1536. }
  1537. },
  1538. {
  1539. name: '所在单位意见 / 完工验收的 payload 互不覆盖',
  1540. test: (t) =>
  1541. runtime.buildNodePayload(t.nodeOf('unit_opinion'), {
  1542. unit_opinion: '单位意见'
  1543. }).unit_opinion === '单位意见' &&
  1544. runtime.buildNodePayload(t.nodeOf('acceptance'), {
  1545. acceptance_opinion: '验收意见'
  1546. }).acceptance_opinion === '验收意见'
  1547. }
  1548. ]
  1549. };
  1550. // 源系统「现场安全措施确认」节点 formData 的默认 20 条(吊装作业),逐条比对
  1551. const HOISTING_SOURCE_MEASURES = [
  1552. '一、二级吊装作业已编制吊装作业方案,已经审查批准;吊装物体形状复杂、刚度小,长径比大、精密贵重,作业条件特殊的三级吊装作业,已编制吊装作业方案,已经审查批准',
  1553. '吊装场所如有含危险物料的设备、管道时,应制定详细吊装方案,并对设备、管道采取有效防护措施,必要时停车,放空物料,置换后再进行吊装作业',
  1554. '作业人员已按规定佩戴个体防护装备',
  1555. '已对起重吊装设备、钢丝绳、揽风绳、链条、吊钩等各种机具进行检查,安全可靠',
  1556. '已明确各自分工',
  1557. '坚守岗位,并统一规定联络信号',
  1558. '将建筑物、构筑物作为锚点,应经所属单位工程管理部门审查校算并批准',
  1559. '吊装绳索、揽风绳、拖拉绳等不应与带电线路接触,并保持安全距离',
  1560. '不应利用管道、管架、电杆、机电设备等作吊装锚点',
  1561. '吊物捆扎坚固,未见绳打结、绳不齐现象,棱角吊物已采取衬垫措施',
  1562. '起重机安全装置灵活好用',
  1563. '吊装作业人员持有有效的法定资格证书',
  1564. '地下通信电(光)缆、局域网络电(光)缆,排水沟的益板,承重吊装机械的负重量已确认,保护措施已落实',
  1565. '起吊物的质量(t)经确认,在吊装机械的承重范围内',
  1566. '在吊装高度的管线、电缆桥架已做好防护措施',
  1567. '作业现场围栏、警戒线、警告牌、夜同警示灯已按要求设置',
  1568. '作业高度和转臂范围内无架空线路',
  1569. '在爆炸危险场所内的作业,机动车排气管已装阻火器',
  1570. '露天作业,环境风力满足作业安全要求',
  1571. '其他相关特殊作业已办理相应安全作业票'
  1572. ];
  1573. // 运行时辅助断言里用的固定数据源
  1574. const RUNTIME_SOURCE = {
  1575. measure_list: [
  1576. { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' }
  1577. ],
  1578. measure_preparer_sign: '/tmp/a.png'
  1579. };
  1580. // ---------------------------------------------------------------------------
  1581. // 四、组件模板 / 脚本体检工具
  1582. // ---------------------------------------------------------------------------
  1583. /**
  1584. * 收集组件脚本里已定义的成员名(props / data / computed / methods)
  1585. * @param {string} script script 源码
  1586. * @returns {Set<string>} 成员名集合
  1587. */
  1588. function collectComponentMembers(script) {
  1589. const names = new Set();
  1590. const ast = parser.parse(script, {
  1591. sourceType: 'module',
  1592. plugins: [
  1593. 'optionalChaining',
  1594. 'objectRestSpread',
  1595. 'nullishCoalescingOperator'
  1596. ]
  1597. });
  1598. let component = null;
  1599. for (const node of ast.program.body) {
  1600. if (node.type === 'ExportDefaultDeclaration') {
  1601. component = node.declaration;
  1602. }
  1603. }
  1604. const collectObjectKeys = (obj) => {
  1605. ((obj && obj.properties) || []).forEach((prop) => {
  1606. if (!prop.key) {
  1607. return;
  1608. }
  1609. names.add(prop.key.name || prop.key.value);
  1610. });
  1611. };
  1612. ((component && component.properties) || []).forEach((prop) => {
  1613. if (!prop.key) {
  1614. return;
  1615. }
  1616. const key = prop.key.name;
  1617. const value = prop.value;
  1618. if (key === 'props' || key === 'methods' || key === 'computed') {
  1619. collectObjectKeys(value);
  1620. return;
  1621. }
  1622. if (key === 'data') {
  1623. // data() { return { ... } }
  1624. const body = value && value.body;
  1625. if (!body) {
  1626. return;
  1627. }
  1628. const ret = body.body.find(
  1629. (stmt) => stmt.type === 'ReturnStatement' && stmt.argument
  1630. );
  1631. if (ret) {
  1632. collectObjectKeys(ret.argument);
  1633. }
  1634. }
  1635. });
  1636. return names;
  1637. }
  1638. /**
  1639. * 取模板里被调用的成员名
  1640. *
  1641. * 注意:Vue 2 编译后的 render 用 with(this) 解析标识符,
  1642. * 方法调用编译成裸函数名(如 getRows(field)),不带 vm. 前缀,
  1643. * 因此这里直接扫描模板表达式,而不是扫描编译产物。
  1644. *
  1645. * @param {string} template 模板源码
  1646. * @returns {object} { calls, errors }
  1647. */
  1648. function collectTemplateCalls(template) {
  1649. const calls = new Set();
  1650. // 1) 插值 {{ expr }}
  1651. // 2) 指令 / 事件绑定属性值: :xxx="expr"、v-xxx="expr"、v-xxx:arg="expr"、@xxx="expr"
  1652. const attrRe = /(?:[:@][\w.-]+|v-[\w-]+(?::[\w.-]+)?)\s*=\s*"([^"]*)"/g;
  1653. const interpRe = /\{\{([\s\S]*?)\}\}/g;
  1654. const exprs = [];
  1655. let m;
  1656. while ((m = attrRe.exec(template)) !== null) {
  1657. exprs.push(m[1]);
  1658. }
  1659. while ((m = interpRe.exec(template)) !== null) {
  1660. exprs.push(m[1]);
  1661. }
  1662. // 全局函数与 Vue 内部函数,不属于组件成员
  1663. const globals = new Set([
  1664. 'String',
  1665. 'Number',
  1666. 'Boolean',
  1667. 'Array',
  1668. 'Object',
  1669. 'JSON',
  1670. 'Math',
  1671. 'Date',
  1672. 'parseInt',
  1673. 'parseFloat',
  1674. 'isNaN',
  1675. 'require'
  1676. ]);
  1677. // 标识符后紧跟 "(" 即为函数调用;排除属性访问(xxx.method())与内部变量
  1678. const callRe = /(^|[^.\w$])([A-Za-z_$][\w$]*)\s*\(/g;
  1679. exprs.forEach((expr) => {
  1680. let hit;
  1681. callRe.lastIndex = 0;
  1682. while ((hit = callRe.exec(expr)) !== null) {
  1683. const name = hit[2];
  1684. if (name.startsWith('$') || name.startsWith('_') || globals.has(name)) {
  1685. continue;
  1686. }
  1687. calls.add(name);
  1688. }
  1689. });
  1690. return { calls, errors: compiler.compile(template).errors || [] };
  1691. }
  1692. // ---------------------------------------------------------------------------
  1693. // 五、逐节校验
  1694. // ---------------------------------------------------------------------------
  1695. console.log('\n[1] 公共模块(flowRuntime / flowMatcher / flowFields)');
  1696. ['flowRuntime.js', 'flowMatcher.js', 'flowFields.js'].forEach((file) => {
  1697. check(file + ' 存在', fs.existsSync(path.join(SRC, file)));
  1698. });
  1699. const RUNTIME_EXPORTS = [
  1700. 'getCurrentTaskInfo',
  1701. 'loadProcessFormVariables',
  1702. 'getFieldKeys',
  1703. 'loadNodeValues',
  1704. 'buildNodeVariables',
  1705. 'buildNodePayload',
  1706. 'mergeExtension',
  1707. 'buildTicketUpdateParams',
  1708. 'parseJsonObject'
  1709. ];
  1710. RUNTIME_EXPORTS.forEach((fn) => {
  1711. check('flowRuntime.js 导出 ' + fn, typeof runtime[fn] === 'function');
  1712. });
  1713. check('flowMatcher.js 导出 matchFlowNode', typeof matcher.matchFlowNode === 'function');
  1714. check('flowFields.js 导出 getFieldKeys', typeof fieldUtils.getFieldKeys === 'function');
  1715. check(
  1716. 'flowRuntime.js 再导出的 getFieldKeys 与 flowFields.js 是同一实现',
  1717. runtime.getFieldKeys === fieldUtils.getFieldKeys
  1718. );
  1719. console.log('\n[2] 八个作业类型的节点定义文件');
  1720. TYPES.forEach((type) => {
  1721. console.log(' -- ' + type.label + '作业(' + type.module + '.js)');
  1722. check(
  1723. type.module + '.js 存在',
  1724. fs.existsSync(path.join(SRC, type.module + '.js'))
  1725. );
  1726. check(
  1727. type.module + '.js 导出 ' + type.nodesConst,
  1728. Array.isArray(type.mod[type.nodesConst]) && type.mod[type.nodesConst].length > 0
  1729. );
  1730. check(
  1731. type.module + '.js 导出 ' + type.matchFn,
  1732. typeof type.mod[type.matchFn] === 'function'
  1733. );
  1734. check(
  1735. type.module + '.js 再导出了 getFieldKeys',
  1736. typeof type.mod.getFieldKeys === 'function'
  1737. );
  1738. // 包装函数确实委托给通用匹配器:同一组候选标识下两者结果一致
  1739. const candidates = []
  1740. .concat(type.nodes.map((n) => n.code))
  1741. .concat(type.nodes.map((n) => n.name))
  1742. .concat(type.nodes.map((n) => n.key))
  1743. .concat([type.flowCode, type.flowCode + '_ONETH', '', null, undefined]);
  1744. const delegated = candidates.every((c) => {
  1745. const a = matcher.matchFlowNode(type.nodes, c);
  1746. const b = type.match(c);
  1747. return (a && a.key) === (b && b.key);
  1748. });
  1749. check(
  1750. type.module + '.js 的 ' + type.matchFn + ' 与 matchFlowNode 对同一组候选标识结果一致',
  1751. delegated
  1752. );
  1753. // 作业类型之间互不污染:节点定义文件只依赖公共模块,不引用其它作业类型的文件
  1754. const ownSource = fs.readFileSync(path.join(SRC, type.module + '.js'), 'utf8');
  1755. const otherTypes = TYPES.filter((t) => t.module !== type.module);
  1756. const foreignImports = otherTypes.filter(
  1757. (t) => ownSource.indexOf("'./" + t.module + "'") >= 0
  1758. );
  1759. check(
  1760. type.module + '.js 不引用其它作业类型的节点定义文件',
  1761. foreignImports.length === 0,
  1762. foreignImports.map((t) => t.module)
  1763. );
  1764. const keys = type.nodes.map((n) => n.key);
  1765. const codes = type.nodes.map((n) => n.code);
  1766. const names = type.nodes.map((n) => n.name);
  1767. check(type.nodesConst + ' 的 key 唯一', new Set(keys).size === keys.length, keys);
  1768. check(type.nodesConst + ' 的 code 唯一', new Set(codes).size === codes.length, codes);
  1769. check(type.nodesConst + ' 的 name 唯一', new Set(names).size === names.length, names);
  1770. check(
  1771. type.nodesConst + ' 的 code 均以 ' + type.codePrefix + ' 开头',
  1772. codes.every((c) => c.indexOf(type.codePrefix) === 0),
  1773. codes.filter((c) => c.indexOf(type.codePrefix) !== 0)
  1774. );
  1775. });
  1776. // 动火作业沿用自己的实现(fireFlowTask.js 已由公共的 flowRuntime.js 取代)
  1777. [
  1778. 'fireFlowNodes.js',
  1779. 'components/fireTicketDetail.vue',
  1780. 'components/flowNodeForm.vue'
  1781. ].forEach((file) => {
  1782. check('动火作业的既有实现文件 ' + file + ' 仍然存在', fs.existsSync(path.join(SRC, file)));
  1783. });
  1784. TYPES.forEach((type) => {
  1785. console.log(
  1786. '\n[3] ' + type.label + '作业环节(源系统 ' + type.flowCode + ' 共 ' + (type.count + 1) + ' 节点,1 级为作业申请模板)'
  1787. );
  1788. check('环节数量为 ' + type.count, type.nodes.length === type.count, {
  1789. len: type.nodes.length
  1790. });
  1791. type.expected.forEach(([code, name], i) => {
  1792. const node = type.nodes[i];
  1793. check(
  1794. '第' + (i + 2) + '级 ' + code + ' / ' + name,
  1795. node.code === code && node.name === name,
  1796. { code: node.code, name: node.name }
  1797. );
  1798. });
  1799. console.log('\n[4] ' + type.label + '作业:与源系统 formJson 一致性');
  1800. FORM_CHECKS[type.key].forEach((spec) => {
  1801. check(
  1802. spec.name,
  1803. spec.test(type),
  1804. spec.extra ? spec.extra(type) : undefined
  1805. );
  1806. });
  1807. console.log('\n[5] ' + type.label + '作业:环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
  1808. MATCH_CHECKS[type.key].forEach((spec) => {
  1809. if (spec.notMatch) {
  1810. const hit = spec.notMatch.map((c) => type.match(c));
  1811. check(
  1812. spec.name,
  1813. hit.every((m) => m === null),
  1814. hit.map((m) => (m && m.key) || null)
  1815. );
  1816. return;
  1817. }
  1818. if (spec.empty) {
  1819. check(spec.name, type.match('', null) === null);
  1820. return;
  1821. }
  1822. const m = type.match(spec.code);
  1823. check(spec.name, m && m.key === spec.key, { key: m && m.key });
  1824. });
  1825. console.log('\n[6] ' + type.label + '作业:运行时辅助(回显 / payload / 流程变量)');
  1826. RUNTIME_CHECKS.common.concat(RUNTIME_CHECKS[type.key] || []).forEach((spec) => {
  1827. check(spec.name, spec.test(type));
  1828. });
  1829. });
  1830. console.log('\n[7] detail.vue:由 FLOW_TYPES 配置表驱动(七类作业各自一个环节组件)');
  1831. const detailSrc = fs.readFileSync(path.join(SRC, 'detail.vue'), 'utf8');
  1832. const templatesSrc = fs.readFileSync(path.join(SRC, 'templates.js'), 'utf8');
  1833. // 作业类型 -> 环节组件(xxxFlowNodes.js 对应 xxxFlow.vue)
  1834. function componentFileOf(t) {
  1835. return t.module.replace(/FlowNodes$/, 'Flow');
  1836. }
  1837. function componentIdentOf(t) {
  1838. const file = componentFileOf(t);
  1839. return file.charAt(0).toUpperCase() + file.slice(1);
  1840. }
  1841. const GENERIC_TYPES = TYPES.filter((t) => t.templateKey);
  1842. const flowTypesStart = detailSrc.indexOf('const FLOW_TYPES = [');
  1843. const flowTypesEnd = detailSrc.indexOf('];', flowTypesStart);
  1844. const flowTypesBlock =
  1845. flowTypesStart >= 0 ? detailSrc.slice(flowTypesStart, flowTypesEnd) : '';
  1846. check('detail.vue 源码包含 FLOW_TYPES 配置表', flowTypesStart >= 0 && flowTypesEnd > flowTypesStart);
  1847. GENERIC_TYPES.forEach((t) => {
  1848. check(
  1849. 'FLOW_TYPES 登记了' + t.label + '作业(key = ' + t.templateKey + ')',
  1850. new RegExp("key:\\s*'" + t.templateKey + "'").test(flowTypesBlock)
  1851. );
  1852. });
  1853. check(
  1854. 'FLOW_TYPES 的每一项都绑定了自己的节点表、匹配函数与环节组件',
  1855. GENERIC_TYPES.every(
  1856. (t) =>
  1857. flowTypesBlock.indexOf(t.nodesConst) >= 0 &&
  1858. flowTypesBlock.indexOf(t.matchFn) >= 0 &&
  1859. flowTypesBlock.indexOf(componentIdentOf(t)) >= 0
  1860. )
  1861. );
  1862. // component 字段的值必须与组件名一致,否则 <component :is> 渲染不出来
  1863. GENERIC_TYPES.forEach((t) => {
  1864. const expect = componentIdentOf(t);
  1865. const m = flowTypesBlock.match(
  1866. new RegExp("key:\\s*'" + t.templateKey + "'[\\s\\S]{0,400}?component:\\s*'([^']+)'")
  1867. );
  1868. check(
  1869. 'FLOW_TYPES[' + t.templateKey + '].component === ' + expect,
  1870. !!m && m[1] === expect,
  1871. m ? m[1] : null
  1872. );
  1873. });
  1874. check(
  1875. '动火作业(fire)不在 FLOW_TYPES 内(沿用自己的实现)',
  1876. !/key:\s*'fire'/.test(flowTypesBlock)
  1877. );
  1878. check(
  1879. 'detail.vue 用 currentFlowType 计算属性定位作业类型',
  1880. /currentFlowType\(\)\s*\{[\s\S]{0,400}FLOW_TYPES\.find/.test(detailSrc)
  1881. );
  1882. check(
  1883. 'detail.vue 用 currentFlowNode 计算属性定位当前处理环节',
  1884. /currentFlowNode\(\)\s*\{[\s\S]{0,400}currentFlowType\.match/.test(detailSrc)
  1885. );
  1886. // 只统计 <template> 块内的标签,注释里出现的 <component 不算
  1887. const detailTemplate = compiler.parseComponent(detailSrc).template.content;
  1888. const dynamicTags = detailTemplate.match(/<component\b/g) || [];
  1889. check(
  1890. 'detail.vue 的模板里只有一个动态组件标签(<component :is>)',
  1891. dynamicTags.length === 1,
  1892. { count: dynamicTags.length }
  1893. );
  1894. check(
  1895. 'detail.vue 用 <component :is="currentFlowType.component"> 渲染对应作业类型的环节组件',
  1896. /:is="currentFlowType\.component"/.test(detailTemplate)
  1897. );
  1898. check(
  1899. 'detail.vue 注册了七个作业类型的环节组件',
  1900. GENERIC_TYPES.every((t) =>
  1901. new RegExp(
  1902. "import\\s+" +
  1903. componentIdentOf(t) +
  1904. "\\s+from\\s+'\\./components\\/" +
  1905. componentFileOf(t) +
  1906. "\\.vue'"
  1907. ).test(detailSrc)
  1908. )
  1909. );
  1910. check(
  1911. 'detail.vue 的取数 / 保存方法对七类作业统一走同一个动态组件',
  1912. /getFlowTableValue\(\)/.test(detailSrc) &&
  1913. /saveFlow\(data\)/.test(detailSrc) &&
  1914. /validateFlowNodeForm\(\)/.test(detailSrc) &&
  1915. /\$refs\.flowView/.test(detailSrc)
  1916. );
  1917. check(
  1918. 'detail.vue 的「后续环节」分发已收敛到 FLOW_TYPES 配置表(旧的逐类型分发逻辑已移除)',
  1919. flowTypesStart >= 0 &&
  1920. !/isConfinedSpaceWork|isHeightWork|isHoistingWork|isElectricityWork|isBreakGroundWork|isBreakRoadWork|isBlindPlateWork/.test(
  1921. detailSrc
  1922. )
  1923. );
  1924. check(
  1925. 'detail.vue:按作业类型分发到各自的后续环节组件',
  1926. /currentFlowType\(\)[\s\S]{0,400}FLOW_TYPES\.find/.test(detailSrc) &&
  1927. /currentFlowNode\(\)[\s\S]{0,400}currentFlowType\.match/.test(detailSrc)
  1928. );
  1929. check(
  1930. 'detail.vue 的默认 tab 是「作业票信息」(不再自动切到「后续环节」)',
  1931. /activeTab:\s*'info'/.test(detailSrc) && !/switchToFlowTab/.test(detailSrc)
  1932. );
  1933. check(
  1934. 'detail.vue:作业票模板已包含临时用电作业(templates.js key = electricity)',
  1935. /key:\s*'electricity'/.test(templatesSrc)
  1936. );
  1937. check(
  1938. 'detail.vue:作业票模板已包含吊装作业(templates.js key = hoisting)',
  1939. /key:\s*'hoisting'/.test(templatesSrc)
  1940. );
  1941. check(
  1942. 'detail.vue 不再引用 xxxFlowTask(各作业类型的运行时辅助已统一到 flowRuntime.js)',
  1943. !/FlowTask/.test(detailSrc)
  1944. );
  1945. check(
  1946. 'detail.vue 引用了公共运行时 flowRuntime.js',
  1947. /from '\.\/flowRuntime'/.test(detailSrc)
  1948. );
  1949. const flowTaskFiles = fs
  1950. .readdirSync(SRC)
  1951. .filter((f) => /FlowTask\.js$/.test(f));
  1952. check(
  1953. '八份 xxxFlowTask.js 已收敛为公共的 flowRuntime.js(不再有作业类型专属的运行时辅助文件)',
  1954. flowTaskFiles.length === 0,
  1955. flowTaskFiles
  1956. );
  1957. GENERIC_TYPES.forEach((t) => {
  1958. check(
  1959. t.label + '作业的环节组件存在(components/' + componentFileOf(t) + '.vue)',
  1960. fs.existsSync(path.join(SRC, 'components', componentFileOf(t) + '.vue'))
  1961. );
  1962. });
  1963. check(
  1964. '通用的 flowNodeView.vue 已下线(环节组件按作业类型各自独立实现)',
  1965. !fs.existsSync(path.join(SRC, 'components', 'flowNodeView.vue'))
  1966. );
  1967. console.log('\n[8] 七个环节组件:接口一致性与模板/脚本一致性');
  1968. // 环节组件必须提供的方法(detail.vue 依赖 validate / getPayload / getVariables)
  1969. const FLOW_VIEW_METHODS = [
  1970. 'validate',
  1971. 'getPayload',
  1972. 'getVariables',
  1973. 'buildForm',
  1974. 'toArray',
  1975. 'update',
  1976. 'updateCell',
  1977. 'addRow',
  1978. 'removeRow',
  1979. 'getRows',
  1980. 'getCodes',
  1981. 'getConfirmColumn',
  1982. 'getConfirmValue',
  1983. 'confirmAll',
  1984. 'buildRules',
  1985. 'buildFieldRules',
  1986. 'validateTableRows',
  1987. 'getRawValue',
  1988. 'getViewCodes',
  1989. 'getViewText',
  1990. 'getViewRows',
  1991. 'getCellText',
  1992. 'getPreviewUrl',
  1993. 'handleImgError',
  1994. 'isCurrent',
  1995. 'nodeDone'
  1996. ];
  1997. const FLOW_VIEW_PROPS = [
  1998. 'node',
  1999. 'nodes',
  2000. 'ticket',
  2001. 'extension',
  2002. 'variables',
  2003. 'taskLabel'
  2004. ];
  2005. GENERIC_TYPES.forEach((t) => {
  2006. const file = componentFileOf(t);
  2007. const source = fs.readFileSync(
  2008. path.join(SRC, 'components', file + '.vue'),
  2009. 'utf8'
  2010. );
  2011. const sfc = compiler.parseComponent(source);
  2012. const templateCheck = collectTemplateCalls(sfc.template.content);
  2013. const members = collectComponentMembers(sfc.script.content);
  2014. const missing = [...templateCheck.calls].filter((name) => !members.has(name));
  2015. check(file + '.vue 模板编译无错误', templateCheck.errors.length === 0, templateCheck.errors);
  2016. check(
  2017. file + '.vue 模板调用的 ' + templateCheck.calls.size + ' 个方法均已定义',
  2018. missing.length === 0,
  2019. missing
  2020. );
  2021. check(
  2022. file + '.vue 对外提供 validate / getPayload / getVariables',
  2023. /validate\(\)\s*\{/.test(sfc.script.content) &&
  2024. /getPayload\(\)\s*\{/.test(sfc.script.content) &&
  2025. /getVariables\(\)\s*\{/.test(sfc.script.content)
  2026. );
  2027. check(
  2028. file + '.vue 定义了约定的全部方法(' + FLOW_VIEW_METHODS.length + ' 个)',
  2029. FLOW_VIEW_METHODS.every((name) => members.has(name)),
  2030. FLOW_VIEW_METHODS.filter((name) => !members.has(name))
  2031. );
  2032. check(
  2033. file + '.vue 的 props 声明了 node / nodes / ticket / extension / variables / taskLabel',
  2034. FLOW_VIEW_PROPS.every((name) => members.has(name)),
  2035. FLOW_VIEW_PROPS.filter((name) => !members.has(name))
  2036. );
  2037. check(
  2038. file + '.vue 的 computed 声明了 hasFields / displayNodes',
  2039. /hasFields\(\)\s*\{/.test(sfc.script.content) &&
  2040. /displayNodes\(\)\s*\{/.test(sfc.script.content)
  2041. );
  2042. check(
  2043. file + '.vue 源码包含 getConfirmColumn / getConfirmValue(子表「全部确认」)',
  2044. /getConfirmColumn\(field\)\s*\{/.test(sfc.script.content) &&
  2045. /getConfirmValue\(column\)\s*\{/.test(sfc.script.content)
  2046. );
  2047. check(
  2048. file + '.vue 源码包含子表日期列渲染分支(column.type === "date")',
  2049. /column\.type === 'date'/.test(sfc.template.content)
  2050. );
  2051. check(
  2052. file + '.vue 模板确实调用了 getConfirmColumn / confirmAll(子表「全部确认」按钮)',
  2053. templateCheck.calls.has('getConfirmColumn') &&
  2054. templateCheck.calls.has('confirmAll')
  2055. );
  2056. check(
  2057. file + '.vue 无表单项的环节给出空态提示(hasFields)',
  2058. /node-empty-tip/.test(sfc.template.content)
  2059. );
  2060. check(
  2061. file + '.vue 的运行时辅助来自公共模块 flowRuntime.js',
  2062. /from '\.\.\/flowRuntime'/.test(sfc.script.content)
  2063. );
  2064. check(
  2065. file + '.vue 的节点 schema 来自本作业类型的 ' + t.module + '.js',
  2066. new RegExp("from '\\.\\./" + t.module + "'").test(sfc.script.content)
  2067. );
  2068. check(
  2069. file + '.vue 未识别到环节时展示当前任务名 / key(便于排查流程模型配置)',
  2070. /taskLabel/.test(source)
  2071. );
  2072. check(
  2073. file + '.vue 沿用同一套环节区块类名(scoped,不影响其它组件)',
  2074. /current-node-box/.test(source) &&
  2075. /all-nodes-box/.test(source) &&
  2076. /node-form-header/.test(source)
  2077. );
  2078. });
  2079. check(
  2080. '作业实施:事中记录表「时间」列类型为 date(动土 / 断路)',
  2081. TYPES.filter(
  2082. (t) => t.templateKey === 'break_ground' || t.templateKey === 'break_road'
  2083. ).every((t) =>
  2084. t
  2085. .nodeOf('execute')
  2086. .fields.find((f) => f.key === 'during_record_list')
  2087. .columns.some((c) => c.key === 'record_time' && c.type === 'date')
  2088. )
  2089. );
  2090. check(
  2091. '作业实施:事中记录表「日期时间」列类型为 date(高处 / 吊装)',
  2092. TYPES.filter(
  2093. (t) => t.templateKey === 'height' || t.templateKey === 'hoisting'
  2094. ).every((t) =>
  2095. t
  2096. .nodeOf('execute')
  2097. .fields.find((f) => f.key === 'during_record_list')
  2098. .columns.some((c) => c.key === 'record_time' && c.type === 'date')
  2099. )
  2100. );
  2101. console.log('\n[9] 时间类字段:必须使用日期时间选择器(不能用输入框)');
  2102. // 可编辑态的环节组件:七类作业的 *Flow.vue + 动火的 flowNodeForm.vue
  2103. const EDITABLE_COMPONENTS = GENERIC_TYPES.map((t) => componentFileOf(t)).concat([
  2104. 'flowNodeForm'
  2105. ]);
  2106. EDITABLE_COMPONENTS.forEach((file) => {
  2107. const source = fs.readFileSync(
  2108. path.join(SRC, 'components', file + '.vue'),
  2109. 'utf8'
  2110. );
  2111. const sfc = compiler.parseComponent(source);
  2112. check(
  2113. file + '.vue 的表单项按 field.type === "date" 渲染为 el-date-picker',
  2114. /v-else-if="field\.type === 'date'"/.test(sfc.template.content) &&
  2115. /<el-date-picker[\s\S]{0,240}field\.type === 'date'/.test(sfc.template.content)
  2116. );
  2117. check(
  2118. file + '.vue 的子表列按 column.type === "date" 渲染为 el-date-picker',
  2119. /v-else-if="column\.type === 'date'"/.test(sfc.template.content) &&
  2120. /<el-date-picker[\s\S]{0,240}column\.type === 'date'/.test(sfc.template.content)
  2121. );
  2122. });
  2123. // schema 层:label 含「时间/日期」的字段与子表列,type 必须是 date
  2124. const TIME_LABEL = /时间|日期/;
  2125. TYPES.forEach((t) => {
  2126. const bad = [];
  2127. t.nodes.forEach((node) => {
  2128. (node.fields || []).forEach((field) => {
  2129. if (field.type === 'table') {
  2130. (field.columns || []).forEach((col) => {
  2131. if (TIME_LABEL.test(col.label || '') && col.type !== 'date') {
  2132. bad.push(
  2133. node.key + ' / ' + field.key + ' / 列「' + col.label + '」type=' + col.type
  2134. );
  2135. }
  2136. });
  2137. } else if (
  2138. TIME_LABEL.test(field.label || '') &&
  2139. field.type !== 'date' &&
  2140. field.type !== 'table'
  2141. ) {
  2142. bad.push(
  2143. node.key + ' / ' + field.key + ' / 「' + field.label + '」type=' + field.type
  2144. );
  2145. }
  2146. });
  2147. });
  2148. check(
  2149. t.module + ':所有含「时间/日期」的字段与子表列类型都是 date',
  2150. bad.length === 0,
  2151. bad
  2152. );
  2153. });
  2154. console.log('\n[10] 作业票内容(基本信息)的手写签名能正常显示');
  2155. // 作业票内容里存的是签名文件存储路径(main/xxx/signature_1.png),
  2156. // 是相对路径,直接当 img src 用会被浏览器按页面地址解析,必然加载失败,
  2157. // 必须拼上文件服务地址(@/utils/file 的 getFilePreviewUrl)才能显示。
  2158. const tcvPath = path.join(SRC, 'components/ticketContentView.vue');
  2159. const tcvSource = fs.readFileSync(tcvPath, 'utf8');
  2160. const tcv = compiler.parseComponent(tcvSource);
  2161. const tcvCheck = collectTemplateCalls(tcv.template.content);
  2162. const tcvMembers = collectComponentMembers(tcv.script.content);
  2163. check('ticketContentView.vue 模板编译无错误', tcvCheck.errors.length === 0, tcvCheck.errors);
  2164. const tcvMissing = [...tcvCheck.calls].filter((name) => !tcvMembers.has(name));
  2165. check(
  2166. `ticketContentView.vue 模板调用的 ${tcvCheck.calls.size} 个方法均已定义`,
  2167. tcvMissing.length === 0,
  2168. tcvMissing
  2169. );
  2170. check(
  2171. '签名图片地址走 getPreviewUrl(拼文件服务地址),不是把存储路径直接当 src',
  2172. /:src="getPreviewUrl\(getValue\(field\)\)"/.test(tcv.template.content) &&
  2173. !/:src="getValue\(field\)"/.test(tcv.template.content)
  2174. );
  2175. check(
  2176. 'ticketContentView.vue 引入了 @/utils/file 的 getFilePreviewUrl',
  2177. /import \{ getFilePreviewUrl as buildFilePreviewUrl \} from '@\/utils\/file'/.test(
  2178. tcv.script.content
  2179. ) &&
  2180. /getPreviewUrl\(path\)\s*\{\s*return buildFilePreviewUrl\(path\)/.test(
  2181. tcv.script.content
  2182. )
  2183. );
  2184. check(
  2185. '签名图片加载失败时退回显示原始值(imgErrors + handleImgError)',
  2186. /imgErrors/.test(tcv.script.content) &&
  2187. /handleImgError\(field\)/.test(tcv.script.content) &&
  2188. /@error="handleImgError\(field\)"/.test(tcv.template.content)
  2189. );
  2190. check(
  2191. 'isImage 兼容完整地址与 data/blob 地址(不只是图片扩展名)',
  2192. /\^\(https\?:\|data:\|blob:\)/i.test(tcv.script.content) &&
  2193. /svg/.test(tcv.script.content)
  2194. );
  2195. const blindPlateBlock = templatesSrc.slice(templatesSrc.indexOf("key: 'blind_plate'"));
  2196. // 盲板抽堵作业票基本信息里有「位置图编制人签字」字段(type: sign)
  2197. check(
  2198. '盲板抽堵作业票基本信息里有「位置图编制人签字」字段(type: sign)',
  2199. /id: 'plate_drafter'/.test(blindPlateBlock) &&
  2200. /label: '位置图编制人签字'/.test(blindPlateBlock) &&
  2201. /type: 'sign'/.test(blindPlateBlock)
  2202. );
  2203. check(
  2204. '盲板抽堵作业票基本信息里有「盲板位置图」附件字段(type: file)',
  2205. /id: 'plate_drawing'/.test(blindPlateBlock) &&
  2206. /label: '盲板位置图'/.test(blindPlateBlock) &&
  2207. /type: 'file'/.test(blindPlateBlock)
  2208. );
  2209. console.log('\n[11] 作业类型名称匹配:全等优先 + 包含兜底');
  2210. const workTypes = require(path.join(OUT, 'workTypes.js'));
  2211. const templatesMod = require(path.join(OUT, 'templates.js'));
  2212. check(
  2213. 'workTypes.js 导出 findByWorkTypeName',
  2214. typeof workTypes.findByWorkTypeName === 'function'
  2215. );
  2216. check(
  2217. 'workTypes.js 导出 isSameWorkTypeName',
  2218. typeof workTypes.isSameWorkTypeName === 'function'
  2219. );
  2220. check(
  2221. 'workTypes.js 导出 matchBuiltInWorkType',
  2222. typeof workTypes.matchBuiltInWorkType === 'function'
  2223. );
  2224. const findByWorkTypeName = workTypes.findByWorkTypeName;
  2225. const matchBuiltInWorkType = workTypes.matchBuiltInWorkType;
  2226. // 第一轮:名称完全相等
  2227. [
  2228. '动火作业',
  2229. '受限空间作业',
  2230. '高处作业',
  2231. '吊装作业',
  2232. '临时用电作业',
  2233. '动土作业',
  2234. '断路作业',
  2235. '盲板抽堵作业'
  2236. ].forEach((dictName) => {
  2237. const hit = matchBuiltInWorkType('', dictName);
  2238. check(
  2239. '字典值「' + dictName + '」按名称全等命中内置同名类型',
  2240. !!hit && hit.name === dictName,
  2241. hit ? hit.name : null
  2242. );
  2243. });
  2244. // 第二轮:包含关系兜底
  2245. [
  2246. ['动火', '动火作业'],
  2247. ['受限空间', '受限空间作业'],
  2248. ['高处', '高处作业'],
  2249. ['吊装', '吊装作业'],
  2250. ['临时用电', '临时用电作业'],
  2251. ['动土', '动土作业'],
  2252. ['断路', '断路作业'],
  2253. ['盲板抽堵', '盲板抽堵作业']
  2254. ].forEach(([dictName, expect]) => {
  2255. const hit = matchBuiltInWorkType('', dictName);
  2256. check(
  2257. '字典值「' + dictName + '」按包含关系兜底命中内置「' + expect + '」',
  2258. !!hit && hit.name === expect,
  2259. hit ? hit.name : null
  2260. );
  2261. });
  2262. // 全等优先:候选表里存在「更靠前、但只是包含关系」的条目时,全等项仍然胜出
  2263. const exactFirst = findByWorkTypeName(
  2264. [
  2265. { name: '登高作业', tag: 'partial' },
  2266. { name: '高处作业', tag: 'exact' }
  2267. ],
  2268. '高处作业',
  2269. (item) => [item.name]
  2270. );
  2271. check(
  2272. '全等优先:候选表里「登高作业」排在前面时,「高处作业」仍命中全等项',
  2273. !!exactFirst && exactFirst.tag === 'exact'
  2274. );
  2275. // 包含兜底取最具体:多条都能包含时,选名称最长的那条
  2276. const mostSpecific = findByWorkTypeName(
  2277. [
  2278. { name: '动火作业', tag: 'short' },
  2279. { name: '动火特殊作业', tag: 'long' }
  2280. ],
  2281. '特殊作业',
  2282. (item) => [item.name]
  2283. );
  2284. check(
  2285. '包含兜底取最具体:多条都能包含时选名称最长的那条',
  2286. !!mostSpecific && mostSpecific.tag === 'long'
  2287. );
  2288. // 泛词不参与匹配
  2289. ['作业', '类型', '其他', '其它', '作业票', '其它作业'].forEach((generic) => {
  2290. check(
  2291. '泛词「' + generic + '」不命中任何内置作业类型',
  2292. matchBuiltInWorkType('', generic) === null
  2293. );
  2294. });
  2295. // 编码兜底:内置字典码 / 源系统编码
  2296. const byBuiltInCode = matchBuiltInWorkType('3', '');
  2297. check(
  2298. '按内置字典码兜底:workType=3 命中高处作业',
  2299. !!byBuiltInCode && byBuiltInCode.name === '高处作业'
  2300. );
  2301. const bySourceCode = matchBuiltInWorkType('HO_PERMIT_HEIGHT', '');
  2302. check(
  2303. '按源系统编码兜底:workType=HO_PERMIT_HEIGHT 命中高处作业',
  2304. !!bySourceCode && bySourceCode.name === '高处作业'
  2305. );
  2306. const nameFirst = matchBuiltInWorkType('3', '动火作业');
  2307. check(
  2308. '名称优先于编码:workType=3 + 名称「动火作业」命中动火作业',
  2309. !!nameFirst && nameFirst.name === '动火作业'
  2310. );
  2311. check(
  2312. '名称与编码都取不到时返回 null',
  2313. matchBuiltInWorkType('ZZZ', '不存在的作业类型') === null
  2314. );
  2315. // 作业票模板匹配:按名称 / 按源系统编码
  2316. templatesMod.WORK_TICKET_TEMPLATES.forEach((tpl) => {
  2317. const name = (tpl.workTypeNames || [])[0];
  2318. const byName = templatesMod.getWorkTicketTemplate('', name);
  2319. check(
  2320. '作业票模板按名称「' + name + '」命中模板 ' + tpl.key,
  2321. !!byName && byName.key === tpl.key,
  2322. byName ? byName.key : null
  2323. );
  2324. const bySource = templatesMod.getWorkTicketTemplate(tpl.sourceCode, '');
  2325. check(
  2326. '作业票模板按源系统编码 ' + tpl.sourceCode + ' 命中模板 ' + tpl.key,
  2327. !!bySource && bySource.key === tpl.key,
  2328. bySource ? bySource.key : null
  2329. );
  2330. });
  2331. check(
  2332. '作业票模板匹配不到时返回 null',
  2333. templatesMod.getWorkTicketTemplate('ZZZ', '不存在的作业类型') === null
  2334. );
  2335. console.log(
  2336. '\n' +
  2337. (failed
  2338. ? 'FAILED: ' + failed + ' 项'
  2339. : 'ALL PASSED') +
  2340. '(共 ' +
  2341. total +
  2342. ' 条断言)'
  2343. );
  2344. process.exit(failed ? 1 : 0);