| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391 |
- /**
- * 危险作业票「后续环节」统一校验
- *
- * 用法:node scripts/check-dangerous-work-flow-nodes.js
- *
- * 背景:
- * 八类作业票(动火 / 受限空间 / 高处 / 吊装 / 临时用电 / 动土 / 断路 / 盲板抽堵)
- * 的「后续环节」此前是同一份实现复制了八遍(8 份 *FlowNodes.js + 8 份 *FlowTask.js
- * + 7 份 xxxFlow.vue),现已收敛为:
- * - flowRuntime.js 公共运行时(回显 / payload / 流程变量 / 作业票参数)
- * - flowMatcher.js 公共环节匹配器(code -> 别名 -> 名称 -> 双向包含)
- * - flowFields.js 字段取值 key 工具(纯函数,不依赖接口)
- * - 八份 *FlowNodes.js 只描述各自的节点 schema
- * - 七份 components/xxxFlow.vue 各作业类型自己的「后续环节」组件(对外接口一致)
- * - detail.vue 由模块级常量 FLOW_TYPES 配置表驱动,
- * 环节组件由 <component :is="currentFlowType.component"> 渲染
- * 本脚本是原来 7 个 check-*-work-flow-nodes.js 的合并版,
- * 基线里的全部 314 条断言都保留了下来,并补充了针对新结构的断言。
- *
- * 校验内容:
- * 1) 三个公共模块存在且导出齐全
- * 2) 八份节点定义文件各自导出节点表与匹配包装,包装确实委托给 matchFlowNode;
- * 节点表的 key / code / name 唯一,code 以该作业类型的 HO_PERMIT_ 前缀开头
- * 3) 各作业类型的环节数量与「第 N 级 code / name」与源系统一致
- * 4) 各环节的表单设计与源系统 formJson 一致(字段类型 / 列 / 默认清单)
- * 5) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
- * 6) 运行时辅助(回显 / payload / 流程变量)的取值口径正确
- * 7) detail.vue 由 FLOW_TYPES 配置表驱动,七类作业各自的环节组件在位
- * 8) 七个环节组件的对外接口一致性与模板/脚本一致性
- * 9) 时间类字段一律用日期时间选择器渲染,不能落回输入框
- * 10) 作业票内容(基本信息)的手写签名能正常显示
- * 11) 作业类型名称匹配:全等优先 + 包含兜底
- * 9) 作业票内容(基本信息)的手写签名能正常显示
- *
- * 源系统依据:
- * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
- * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=...
- * -> 流程定义 HO_PERMIT_FIRE / HO_PERMIT_CONFINED_SPACE / HO_PERMIT_HEIGHT /
- * HO_PERMIT_HOISTING / HO_PERMIT_ELECTRICITY / HO_PERMIT_BREAK_GROUND /
- * HO_PERMIT_BREAK_ROAD / HO_PERMIT_BLIND_PLATE
- */
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const babel = require('@babel/core');
- const Module = require('module');
- const compiler = require('vue-template-compiler');
- const parser = require('@babel/parser');
- const ROOT = path.resolve(__dirname, '..');
- const SRC = path.join(
- ROOT,
- 'src/views/bpm/handleTask/components/dangerousWorkTicket'
- );
- const OUT = path.join(os.tmpdir(), 'kd-aiot-check-dangerous-work-flow-nodes');
- let failed = 0;
- let total = 0;
- function check(name, cond, extra) {
- total++;
- if (cond) {
- console.log(' PASS ' + name);
- } else {
- failed++;
- console.log(
- ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
- );
- }
- }
- // ---------------------------------------------------------------------------
- // 一、编译 ESM -> CJS,并准备 @/ 桩模块
- // ---------------------------------------------------------------------------
- // 八个作业类型的节点定义文件 + 三个公共模块
- const COMPILE_FILES = [
- 'flowFields.js',
- 'flowMatcher.js',
- 'flowRuntime.js',
- 'workTypes.js',
- 'templates.js',
- 'fireFlowNodes.js',
- 'confinedSpaceFlowNodes.js',
- 'heightFlowNodes.js',
- 'hoistingFlowNodes.js',
- 'electricityFlowNodes.js',
- 'breakGroundFlowNodes.js',
- 'breakRoadFlowNodes.js',
- 'blindPlateFlowNodes.js'
- ];
- fs.mkdirSync(OUT, { recursive: true });
- COMPILE_FILES.forEach((file) => {
- const code = fs.readFileSync(path.join(SRC, file), 'utf8');
- const res = babel.transformSync(code, {
- filename: file,
- babelrc: false,
- configFile: false,
- plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
- });
- fs.writeFileSync(path.join(OUT, file), res.code);
- });
- // 运行时辅助会引用 @/api/*,这里用桩模块代替,避免校验脚本依赖后端接口
- const STUB = path.join(OUT, 'stub.js');
- fs.writeFileSync(
- STUB,
- 'module.exports = {\n' +
- ' getTaskListByProcessInstanceId: function () { return Promise.resolve([]); },\n' +
- ' getProcessInstance: function () { return Promise.resolve({}); }\n' +
- '};\n'
- );
- const originalResolve = Module._resolveFilename;
- Module._resolveFilename = function (request, ...args) {
- if (request.startsWith('@/')) {
- return STUB;
- }
- return originalResolve.call(this, request, ...args);
- };
- const runtime = require(path.join(OUT, 'flowRuntime.js'));
- const matcher = require(path.join(OUT, 'flowMatcher.js'));
- const fieldUtils = require(path.join(OUT, 'flowFields.js'));
- // ---------------------------------------------------------------------------
- // 二、八类作业的配置表(数据驱动)
- // ---------------------------------------------------------------------------
- const TYPES = [
- {
- key: 'fire',
- label: '动火',
- module: 'fireFlowNodes',
- nodesConst: 'FIRE_FLOW_NODES',
- matchFn: 'matchFireFlowNode',
- measuresConst: 'DEFAULT_FIRE_MEASURES',
- codePrefix: 'HO_PERMIT_FIRE_',
- flowCode: 'HO_PERMIT_FIRE',
- // 动火沿用自己的实现,不在 detail.vue 的 FLOW_TYPES 配置表内
- templateKey: null,
- count: 11,
- expected: [
- ['HO_PERMIT_FIRE_TWO', '风险辨识'],
- ['HO_PERMIT_FIRE_THREETH', '气体分析'],
- ['HO_PERMIT_FIRE_FOURTH', '现场安全措施落实'],
- ['HO_PERMIT_FIRE_FIVETH', '安全交底'],
- ['HO_PERMIT_FIRE_SIXTH', '作业负责人意见'],
- ['HO_PERMIT_FIRE_SEVENTH', '所在单位意见'],
- ['HO_PERMIT_FIRE_EIGHTTH', '安全管理部门意见'],
- ['HO_PERMIT_FIRE_NINETH', '动火审批人意见'],
- ['HO_PERMIT_FIRE_TENTH', '动火前验票'],
- ['HO_PERMIT_FIRE_ELETH', '作业实施'],
- ['HO_PERMIT_FIRE_TWETEENTH', '完工验收']
- ]
- },
- {
- key: 'confined_space',
- label: '受限空间',
- module: 'confinedSpaceFlowNodes',
- nodesConst: 'CONFINED_SPACE_FLOW_NODES',
- matchFn: 'matchConfinedSpaceFlowNode',
- measuresConst: 'DEFAULT_CONFINED_SPACE_MEASURES',
- codePrefix: 'HO_PERMIT_CONFINED_SPACE_',
- flowCode: 'HO_PERMIT_CONFINED_SPACE',
- templateKey: 'confined_space',
- count: 7,
- expected: [
- ['HO_PERMIT_CONFINED_SPACE_TWOTH', '风险辨识'],
- ['HO_PERMIT_CONFINED_SPACE_THREETH', '气体分析'],
- ['HO_PERMIT_CONFINED_SPACE_FOURTH', '现场安全措施确认'],
- ['HO_PERMIT_CONFINED_SPACE_FIVETH', '安全交底'],
- ['HO_PERMIT_CONFINED_SPACE_SIXTH', '所在单位意见'],
- ['HO_PERMIT_CONFINED_SPACE_SEVENTH', '作业实施'],
- ['HO_PERMIT_CONFINED_SPACE_EIGHTTH', '完工验收']
- ]
- },
- {
- key: 'height',
- label: '高处',
- module: 'heightFlowNodes',
- nodesConst: 'HEIGHT_FLOW_NODES',
- matchFn: 'matchHeightFlowNode',
- measuresConst: 'DEFAULT_HEIGHT_MEASURES',
- codePrefix: 'HO_PERMIT_HEIGHT_',
- flowCode: 'HO_PERMIT_HEIGHT',
- templateKey: 'height',
- count: 9,
- expected: [
- ['HO_PERMIT_HEIGHT_TWOTH', '风险辨识'],
- ['HO_PERMIT_HEIGHT_THREETH', '现场安全措施确认'],
- ['HO_PERMIT_HEIGHT_FOURTH', '安全交底'],
- ['HO_PERMIT_HEIGHT_FIVETH', '作业负责人意见'],
- ['HO_PERMIT_HEIGHT_SIXTH', '所在单位意见'],
- ['HO_PERMIT_HEIGHT_SEVENTH', '审核部门意见'],
- ['HO_PERMIT_HEIGHT_EIGHTTH', '审批部门意见'],
- ['HO_PERMIT_HEIGHT_NINETH', '作业实施'],
- ['HO_PERMIT_HEIGHT_TENTH', '完工验收']
- ]
- },
- {
- key: 'hoisting',
- label: '吊装',
- module: 'hoistingFlowNodes',
- nodesConst: 'HOISTING_FLOW_NODES',
- matchFn: 'matchHoistingFlowNode',
- measuresConst: 'DEFAULT_HOISTING_MEASURES',
- codePrefix: 'HO_PERMIT_HOISTING_',
- flowCode: 'HO_PERMIT_HOISTING',
- templateKey: 'hoisting',
- count: 9,
- expected: [
- ['HO_PERMIT_HOISTING_TWOTH', '风险辨识'],
- ['HO_PERMIT_HOISTING_THREETH', '现场安全措施确认'],
- ['HO_PERMIT_HOISTING_FOURTH', '安全交底'],
- ['HO_PERMIT_HOISTING_FIVETH', '作业负责人意见'],
- ['HO_PERMIT_HOISTING_SIXTH', '所在单位意见'],
- ['HO_PERMIT_HOISTING_SEVENTH', '审核部门意见'],
- ['HO_PERMIT_HOISTING_EIGHTTH', '审批部门意见'],
- ['HO_PERMIT_HOISTING_NINETH', '作业实施'],
- ['HO_PERMIT_HOISTING_TENTH', '完工验收']
- ]
- },
- {
- key: 'electricity',
- label: '临时用电',
- module: 'electricityFlowNodes',
- nodesConst: 'ELECTRICITY_FLOW_NODES',
- matchFn: 'matchElectricityFlowNode',
- measuresConst: 'DEFAULT_ELECTRICITY_MEASURES',
- codePrefix: 'HO_PERMIT_ELECTRICITY_',
- flowCode: 'HO_PERMIT_ELECTRICITY',
- templateKey: 'electricity',
- count: 9,
- expected: [
- ['HO_PERMIT_ELECTRICITY_TWOTH', '风险辨识'],
- ['HO_PERMIT_ELECTRICITY_THREETH', '气体分析'],
- ['HO_PERMIT_ELECTRICITY_FOURTH', '危害辨识'],
- ['HO_PERMIT_ELECTRICITY_FIVETH', '现场安全措施确认'],
- ['HO_PERMIT_ELECTRICITY_SIXTH', '安全交底'],
- ['HO_PERMIT_ELECTRICITY_SEVENTH', '作业负责人意见'],
- ['HO_PERMIT_ELECTRICITY_EIGHTTH', '用电单位意见'],
- ['HO_PERMIT_ELECTRICITY_NINETH', '作业实施'],
- ['HO_PERMIT_ELECTRICITY_TENTH', '完工验收']
- ]
- },
- {
- key: 'break_ground',
- label: '动土',
- module: 'breakGroundFlowNodes',
- nodesConst: 'BREAK_GROUND_FLOW_NODES',
- matchFn: 'matchBreakGroundFlowNode',
- measuresConst: 'DEFAULT_BREAK_GROUND_MEASURES',
- codePrefix: 'HO_PERMIT_BREAK_GROUND_',
- flowCode: 'HO_PERMIT_BREAK_GROUND',
- templateKey: 'break_ground',
- count: 9,
- expected: [
- ['HO_PERMIT_BREAK_GROUND_TWOTH', '风险辨识'],
- ['HO_PERMIT_BREAK_GROUND_THREETH', '现场安全措施确认'],
- ['HO_PERMIT_BREAK_GROUND_FOURTH', '安全交底'],
- ['HO_PERMIT_BREAK_GROUND_FIVETH', '作业负责人意见'],
- ['HO_PERMIT_BREAK_GROUND_SIXTH', '所在单位意见'],
- ['HO_PERMIT_BREAK_GROUND_SEVENTH', '有关部门会签意见'],
- ['HO_PERMIT_BREAK_GROUND_EIGHTTH', '审批部门意见'],
- ['HO_PERMIT_BREAK_GROUND_NINETH', '作业实施'],
- ['HO_PERMIT_BREAK_GROUND_TENTH', '完工验收']
- ]
- },
- {
- key: 'break_road',
- label: '断路',
- module: 'breakRoadFlowNodes',
- nodesConst: 'BREAK_ROAD_FLOW_NODES',
- matchFn: 'matchBreakRoadFlowNode',
- measuresConst: 'DEFAULT_BREAK_ROAD_MEASURES',
- codePrefix: 'HO_PERMIT_BREAK_ROAD_',
- flowCode: 'HO_PERMIT_BREAK_ROAD',
- templateKey: 'break_road',
- count: 9,
- expected: [
- ['HO_PERMIT_BREAK_ROAD_TWOTH', '风险辨识'],
- ['HO_PERMIT_BREAK_ROAD_THREETH', '现场安全措施确认'],
- ['HO_PERMIT_BREAK_ROAD_FOURTH', '安全交底'],
- ['HO_PERMIT_BREAK_ROAD_FIVETH', '作业负责人意见'],
- ['HO_PERMIT_BREAK_ROAD_SIXTH', '所在单位意见'],
- ['HO_PERMIT_BREAK_ROAD_SEVENTH', '消防、安全部门意见'],
- ['HO_PERMIT_BREAK_ROAD_EIGHTTH', '审批部门意见'],
- ['HO_PERMIT_BREAK_ROAD_NINETH', '作业实施'],
- ['HO_PERMIT_BREAK_ROAD_TENTH', '完工验收']
- ]
- },
- {
- key: 'blind_plate',
- label: '盲板抽堵',
- module: 'blindPlateFlowNodes',
- nodesConst: 'BLIND_PLATE_FLOW_NODES',
- matchFn: 'matchBlindPlateFlowNode',
- measuresConst: 'DEFAULT_BLIND_PLATE_MEASURES',
- codePrefix: 'HO_PERMIT_BLIND_PLATE_',
- flowCode: 'HO_PERMIT_BLIND_PLATE',
- templateKey: 'blind_plate',
- count: 7,
- expected: [
- ['HO_PERMIT_BLIND_PLATE_TWOTH', '风险辨识'],
- ['HO_PERMIT_BLIND_PLATE_THREETH', '现场安全措施确认'],
- ['HO_PERMIT_BLIND_PLATE_FOURTH', '安全交底'],
- ['HO_PERMIT_BLIND_PLATE_FIVETH', '作业负责人意见'],
- ['HO_PERMIT_BLIND_PLATE_SIXTH', '所在单位意见'],
- ['HO_PERMIT_BLIND_PLATE_SEVENTH', '作业实施'],
- ['HO_PERMIT_BLIND_PLATE_EIGHTTH', '完工验收']
- ]
- }
- ];
- // 载入八份节点定义文件,组装每个作业类型的上下文
- TYPES.forEach((type) => {
- type.mod = require(path.join(OUT, type.module + '.js'));
- type.nodes = type.mod[type.nodesConst];
- type.measures = type.mod[type.measuresConst];
- type.match = type.mod[type.matchFn];
- type.nodeOf = (k) => type.nodes.find((n) => n.key === k);
- type.colsOf = (k, fk) => {
- const node = type.nodeOf(k);
- const field = node.fields.find((f) => f.key === fk);
- return (field.columns || []).map((c) => c.label);
- };
- type.colOf = (k, fk, ck) => {
- const node = type.nodeOf(k);
- const field = node.fields.find((f) => f.key === fk);
- return (field.columns || []).find((c) => c.key === ck);
- };
- });
- // ---------------------------------------------------------------------------
- // 三、各作业类型的断言清单(按类型数据驱动)
- // ---------------------------------------------------------------------------
- /**
- * 生成「意见 + 签名」类环节的断言(只校验字段数量与类型)
- * @param {Array} list [节点 key, 环节标题] 的列表
- */
- function opinionChecks(list) {
- return list.map((item) => ({
- name: item[1] + ':意见 + 签名',
- test: (t) => {
- const node = t.nodeOf(item[0]);
- return (
- node.fields.length === 2 &&
- node.fields[0].type === 'textarea' &&
- node.fields[1].type === 'sign'
- );
- },
- extra: (t) => ({ labels: t.nodeOf(item[0]).fields.map((f) => f.label) })
- }));
- }
- /**
- * 生成「意见 + 签名」类环节的断言(同时校验字段标题)
- * @param {Array} list [节点 key, 环节标题, 签字标题] 的列表
- */
- function titledOpinionChecks(list) {
- return list.map((item) => ({
- name: item[1] + ':意见 + 签名',
- test: (t) => {
- const node = t.nodeOf(item[0]);
- return (
- node.fields.length === 2 &&
- node.fields[0].type === 'textarea' &&
- node.fields[0].label === item[1] &&
- node.fields[1].type === 'sign' &&
- node.fields[1].label === item[2]
- );
- },
- extra: (t) => ({ labels: t.nodeOf(item[0]).fields.map((f) => f.label) })
- }));
- }
- /**
- * 「现场安全措施确认」表名 + 3 列的断言(八个作业类型里除动火外口径一致)
- */
- const measureTableCheck = {
- name: '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)',
- test: (t) =>
- t.nodeOf('measure').fields[0].label === '安全措施确认表' &&
- JSON.stringify(t.colsOf('measure', 'measure_list')) ===
- JSON.stringify(['安全措施', '是否确认', '确认人'])
- };
- /**
- * 「现场安全措施确认」编制人签字 + 确认人签字的断言
- */
- const measureSignCheck = {
- name: '现场安全措施确认:编制人签字 + 确认人签字',
- test: (t) =>
- JSON.stringify(t.nodeOf('measure').fields.map((f) => f.label)) ===
- JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字'])
- };
- /**
- * 「默认行挂在 measure_list 字段上(defaultRows)」的断言
- * @param {number} count 默认清单条数
- */
- function defaultRowsCheck(count) {
- return {
- name: '默认行挂在 measure_list 字段上(defaultRows)',
- test: (t) =>
- Array.isArray(t.nodeOf('measure').fields[0].defaultRows) &&
- t.nodeOf('measure').fields[0].defaultRows.length === count &&
- t.nodeOf('measure').fields[0].defaultRows[0].measure_content ===
- t.measures[0]
- };
- }
- /**
- * 「事中记录表 3 列」的断言
- * @param {string[]} labels 列标题
- */
- function duringRecordCheck(labels) {
- return {
- name: '事中记录表 3 列(' + labels.join('/') + ')',
- test: (t) =>
- JSON.stringify(t.colsOf('execute', 'during_record_list')) ===
- JSON.stringify(labels)
- };
- }
- // 与源系统 formJson 一致性的断言(逐类型)
- const FORM_CHECKS = {
- fire: [
- {
- name: '风险辨识:结果为多选 20 项与源系统完全一致',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].type === 'checkbox' &&
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害'
- );
- }
- },
- {
- name: '安全交底:三个签字(安全交底人签字 / 接受交底人签字 / 监护人签字)',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字', '监护人签字'])
- },
- {
- name: '安全措施默认清单 15 条与源系统 formData 逐条一致',
- test: (t) =>
- t.measures.length === 15 &&
- t.measures[0] ===
- '动火设备内部构件清洗干净,蒸汽吹扫或水洗、置换合格,达到动火条件' &&
- t.measures[14] ===
- '其他相关特殊作业已办理相应安全作业票,作业现场四周已设立警戒区'
- }
- ],
- confined_space: [
- {
- name: '气体分析:表名「气体分析记录表」',
- test: (t) => t.nodeOf('gas_analysis').fields[0].label === '气体分析记录表'
- },
- {
- name: '气体分析:6 列且顺序与源系统一致(分析项目/气体名称/合格标准/取样分析时间/分析部位/分析人)',
- test: (t) =>
- JSON.stringify(t.colsOf('gas_analysis', 'gas_analysis_list')) ===
- JSON.stringify([
- '分析项目',
- '气体名称',
- '合格标准',
- '取样分析时间',
- '分析部位',
- '分析人'
- ]),
- extra: (t) => t.colsOf('gas_analysis', 'gas_analysis_list')
- },
- {
- name: '气体分析:分析项目 3 个选项(有毒有害气体/可燃气体/氧气含量)',
- test: (t) =>
- JSON.stringify(
- t.nodeOf('gas_analysis').fields[0].columns[0].options.map((o) => o.label)
- ) === JSON.stringify(['有毒有害气体', '可燃气体', '氧气含量'])
- },
- measureTableCheck,
- measureSignCheck,
- {
- name: '安全交底:只有 2 个签字(源系统无监护人签字)',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字'])
- },
- {
- name: '所在单位意见:意见 + 签字',
- test: (t) => {
- const node = t.nodeOf('unit_opinion');
- return (
- node.fields.length === 2 &&
- node.fields[0].type === 'textarea' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '作业实施:实施时间 + 结束时间 + 事中记录表 + 气体分析记录表',
- test: (t) =>
- JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify([
- '作业实施时间',
- '作业结束时间',
- '事中记录表',
- '气体分析记录表'
- ])
- },
- {
- name: '作业实施:气体分析记录表 6 列与源系统一致(时间标签为「气体取样分析时间」)',
- test: (t) =>
- JSON.stringify(t.colsOf('execute', 'gas_record_list')) ===
- JSON.stringify([
- '分析项目',
- '气体名称',
- '合格标准',
- '气体取样分析时间',
- '分析部位',
- '分析人'
- ]),
- extra: (t) => t.colsOf('execute', 'gas_record_list')
- },
- duringRecordCheck(['时间', '记录内容', '记录人']),
- {
- name: '完工验收:意见 + 签字',
- test: (t) => {
- const node = t.nodeOf('acceptance');
- return (
- node.fields.length === 2 &&
- node.fields[0].label === '意见' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '风险辨识:多选 20 项与源系统完全一致',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害'
- );
- }
- },
- {
- name: '风险辨识:说明字段别名不与多选框标签冲突',
- test: (t) => !t.nodeOf('risk').fields[1].alias.includes('风险辨识结果')
- },
- {
- name: '安全措施默认清单 14 条与源系统 formData 逐条一致',
- test: (t) =>
- t.measures.length === 14 &&
- t.measures[0] ===
- '盛装过有毒、可燃物料的受限空间,所有与受限空间有联系的阀门、管线已加盲板隔离,并落实盲板责任人,未采用水封或关闭阀门代替盲板' &&
- t.measures[3] ===
- '转动设备已切断电源,电源开关处已加锁并悬挂“禁止合闸”标志牌' &&
- t.measures[13] === '其他相关特殊作业已办理相应安全作业票'
- },
- defaultRowsCheck(14)
- ],
- height: [
- {
- name: '风险辨识:结果为单选(源系统 radio,受限空间为多选)',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].type === 'radio' &&
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害'
- );
- }
- },
- {
- name: '风险辨识:说明字段为单行文本(源系统 input)',
- test: (t) => t.nodeOf('risk').fields[1].type === 'text'
- },
- {
- name: '风险辨识:说明字段别名不与单选框 key 冲突',
- test: (t) =>
- !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
- !t.nodeOf('risk').fields[1].alias.includes('riskResult')
- },
- measureTableCheck,
- measureSignCheck,
- {
- name: '安全交底:源系统未配置表单项,按同族流程惯例补齐「安全交底人签字 + 接受交底人签字」',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
- t.nodeOf('brief').fields.every((f) => f.type === 'sign')
- },
- ...opinionChecks([
- ['leader_opinion', '作业负责人意见'],
- ['unit_opinion', '所在单位意见'],
- ['audit_opinion', '审核部门意见'],
- ['approver_opinion', '审批部门意见']
- ]),
- {
- name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
- test: (t) =>
- JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
- },
- duringRecordCheck(['日期时间', '记录内容', '记录人']),
- {
- name: '完工验收:意见 + 签名',
- test: (t) => {
- const node = t.nodeOf('acceptance');
- return (
- node.fields.length === 2 &&
- node.fields[0].label === '意见' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '安全措施默认清单 14 条与源系统 formData 逐条一致',
- test: (t) =>
- t.measures.length === 14 &&
- t.measures[0] === '作业人员身体条件符合要求' &&
- t.measures[2] ===
- '作业人员佩戴符合标准要求的安全帽、安全带,有可能散发有毒气体的场所携带正压式空气呼吸器或面罩备用' &&
- t.measures[10] === '30 m 以上高处作业时,作业人员已配备通信、联络工具' &&
- t.measures[13] === '其他相关特殊作业已办理相应安全作业票'
- },
- defaultRowsCheck(14)
- ],
- hoisting: [
- {
- name: '风险辨识:结果为单选(源系统 radio,受限空间为多选)',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].type === 'radio' &&
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害'
- );
- }
- },
- {
- name: '风险辨识:说明字段为单行文本(源系统 input)',
- test: (t) => t.nodeOf('risk').fields[1].type === 'text'
- },
- {
- name: '风险辨识:说明字段别名不与单选框 key 冲突',
- test: (t) =>
- !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
- !t.nodeOf('risk').fields[1].alias.includes('riskResult')
- },
- measureTableCheck,
- measureSignCheck,
- {
- name: '安全交底:安全交底人签字 + 接受交底人签字',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
- t.nodeOf('brief').fields.every((f) => f.type === 'sign')
- },
- ...opinionChecks([
- ['leader_opinion', '作业负责人意见'],
- ['unit_opinion', '所在单位意见'],
- ['audit_opinion', '审核部门意见'],
- ['approver_opinion', '审批部门意见']
- ]),
- {
- name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
- test: (t) =>
- JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
- },
- duringRecordCheck(['日期时间', '记录内容', '记录人']),
- {
- name: '完工验收:意见 + 签名',
- test: (t) => {
- const node = t.nodeOf('acceptance');
- return (
- node.fields.length === 2 &&
- node.fields[0].label === '意见' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '安全措施默认清单 20 条与源系统 formData 逐条一致',
- test: (t) =>
- JSON.stringify(t.measures) === JSON.stringify(HOISTING_SOURCE_MEASURES),
- extra: (t) => ({
- len: t.measures.length,
- first: t.measures[0],
- last: t.measures[t.measures.length - 1]
- })
- },
- defaultRowsCheck(20)
- ],
- electricity: [
- {
- name: '气体分析:表名「气体分析记录表」',
- test: (t) => t.nodeOf('gas_analysis').fields[0].label === '气体分析记录表'
- },
- {
- name: '气体分析:4 列且顺序与源系统一致(分析时间/分析点/分析结果/分析人)',
- test: (t) =>
- JSON.stringify(t.colsOf('gas_analysis', 'gas_analysis_list')) ===
- JSON.stringify(['分析时间', '分析点', '分析结果', '分析人']),
- extra: (t) => t.colsOf('gas_analysis', 'gas_analysis_list')
- },
- {
- name: '气体分析:分析时间为日期类型、其余为多行文本(与源系统一致)',
- test: (t) => {
- const c = t.nodeOf('gas_analysis').fields[0].columns;
- return (
- c[0].type === 'date' &&
- c[1].type === 'textarea' &&
- c[2].type === 'textarea' &&
- c[3].type === 'textarea'
- );
- }
- },
- {
- name: '危害辨识:多选 + 单行文本说明(源系统该节点表单复制自风险辨识)',
- test: (t) => {
- const f = t.nodeOf('hazard').fields;
- return (
- f[0].type === 'checkbox' && f[0].options.length === 20 && f[1].type === 'text'
- );
- }
- },
- {
- name: '风险辨识:多选 20 项 + 单行文本说明(源系统 checkbox + input)',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].type === 'checkbox' &&
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害' &&
- f[1].type === 'text'
- );
- }
- },
- {
- name: '风险辨识:说明字段别名不与多选框标签冲突',
- test: (t) => !t.nodeOf('risk').fields[1].alias.includes('风险辨识结果')
- },
- measureTableCheck,
- measureSignCheck,
- {
- name: '是否确认选项为 不涉及 / 已确认',
- test: (t) =>
- JSON.stringify(
- t.nodeOf('measure').fields[0].columns[1].options.map((o) => o.label)
- ) === JSON.stringify(['不涉及', '已确认'])
- },
- {
- name: '安全交底:2 个签字(安全交底人签字 / 接受交底人签字)',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字'])
- },
- {
- name: '作业负责人意见:意见 + 签字',
- test: (t) => {
- const node = t.nodeOf('leader_opinion');
- return (
- node.fields.length === 2 &&
- node.fields[0].type === 'textarea' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '用电单位意见:意见 + 签字(不是受限空间的「所在单位意见」)',
- test: (t) => {
- const node = t.nodeOf('electric_unit_opinion');
- return (
- node.fields.length === 2 &&
- node.fields[0].type === 'textarea' &&
- node.fields[1].type === 'sign' &&
- !t.nodes.some((n) => n.name === '所在单位意见')
- );
- }
- },
- {
- name: '作业实施:开始时间 + 结束时间 + 事中记录表 + 可燃气体分析记录表',
- test: (t) =>
- JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify([
- '作业实施开始时间',
- '作业实施结束时间',
- '事中记录表',
- '可燃气体分析记录表'
- ])
- },
- duringRecordCheck(['时间', '记录内容', '记录人']),
- {
- name: '可燃气体分析记录表 4 列(分析时间/分析点/分析结果/分析人)',
- test: (t) =>
- JSON.stringify(t.colsOf('execute', 'gas_record_list')) ===
- JSON.stringify(['分析时间', '分析点', '分析结果', '分析人'])
- },
- {
- name: '完工验收:意见 + 签字',
- test: (t) => {
- const node = t.nodeOf('acceptance');
- return (
- node.fields.length === 2 &&
- node.fields[0].label === '意见' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '安全措施默认清单 13 条与源系统 formData 逐条一致',
- test: (t) =>
- t.measures.length === 13 &&
- t.measures[0] === '作业人员持有电工作业操作证' &&
- t.measures[2] === '上级开关已断电、加锁,并挂安全警示标牌' &&
- t.measures[9] ===
- '临时用电设施已装配漏电保护器,移动工具、手持工具已采取防漏电的安全措施(一机一闸一保护)' &&
- t.measures[12] === '作业场所已进行气体检测且符合作业安全要求'
- },
- defaultRowsCheck(13),
- // 风险辨识 / 危害辨识 取值 key 不冲突
- {
- name: '两个环节的字段 key 无交集',
- test: (t) => {
- const riskKeys = t.nodeOf('risk').fields.map((f) => f.key);
- const hazardKeys = t.nodeOf('hazard').fields.map((f) => f.key);
- return riskKeys.every((k) => !hazardKeys.includes(k));
- },
- extra: (t) => ({
- riskKeys: t.nodeOf('risk').fields.map((f) => f.key),
- hazardKeys: t.nodeOf('hazard').fields.map((f) => f.key)
- })
- },
- {
- name: '危害辨识使用 hazard_ 前缀',
- test: (t) =>
- t.nodeOf('hazard')
- .fields.map((f) => f.key)
- .every((k) => k.startsWith('hazard_')),
- extra: (t) => t.nodeOf('hazard').fields.map((f) => f.key)
- },
- {
- name: '危害辨识的别名不会命中风险辨识的取值 key',
- test: (t) => {
- const hazardAlias = t
- .nodeOf('hazard')
- .fields.flatMap((f) => f.alias || []);
- return (
- !hazardAlias.includes('risk_result') &&
- !hazardAlias.includes('risk_result_desc')
- );
- }
- }
- ],
- break_ground: [
- {
- name: '风险辨识:结果为多选 20 项(源系统 checkbox)',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].type === 'checkbox' &&
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害'
- );
- }
- },
- {
- name: '风险辨识:说明字段为单行文本(源系统 input)',
- test: (t) => t.nodeOf('risk').fields[1].type === 'text'
- },
- {
- name: '风险辨识:说明字段别名不与多选框 key 冲突',
- test: (t) =>
- !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
- !t.nodeOf('risk').fields[1].alias.includes('riskResult')
- },
- measureTableCheck,
- measureSignCheck,
- {
- name: '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
- test: (t) =>
- t
- .colOf('measure', 'measure_list', 'measure_confirm')
- .options.map((o) => o.label)
- .join(',') === '不涉及,已确认'
- },
- {
- name: '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
- t.nodeOf('brief').fields.every((f) => f.type === 'sign')
- },
- ...opinionChecks([
- ['leader_opinion', '作业负责人意见'],
- ['unit_opinion', '所在单位意见'],
- ['countersign_opinion', '有关部门会签意见'],
- ['approver_opinion', '审批部门意见']
- ]),
- {
- name: '意见类环节的取值 key 互不相同(源系统四处复制同一 model,不能互相覆盖)',
- test: (t) =>
- new Set(
- t.nodes
- .filter((n) => n.fields[0].type === 'textarea')
- .map((n) => n.fields[0].key)
- ).size === 5
- },
- {
- name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
- test: (t) =>
- JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
- },
- duringRecordCheck(['时间', '记录内容', '记录人']),
- {
- name: '完工验收:意见 + 签名',
- test: (t) => {
- const node = t.nodeOf('acceptance');
- return (
- node.fields.length === 2 &&
- node.fields[0].label === '意见' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '安全措施默认清单 10 条与源系统 formData 逐条一致',
- test: (t) =>
- t.measures.length === 10 &&
- t.measures[0] ===
- '地下电力电缆、通信电(光)缆、局域网络电(光)缆已确认,保护措施已落实' &&
- t.measures[4] === '已进行放坡处理和固壁支撑' &&
- t.measures[6] ===
- '现场夜间有充足照明:A.36 V.24V.12 V 防水型灯:B.36V、24 V.12 V 防爆型灯' &&
- t.measures[8] ===
- '易燃易爆、有毒气体存在的场所动土深度超过1.2m,已按照受限空间作业要求采取了措施' &&
- t.measures[9] === '其他相关特殊作业已办理相应安全作业票'
- },
- defaultRowsCheck(10)
- ],
- break_road: [
- {
- name: '风险辨识:结果为多选 20 项(源系统 checkbox)',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].type === 'checkbox' &&
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害'
- );
- }
- },
- {
- name: '风险辨识:说明字段为多行文本(源系统 textarea)',
- test: (t) => t.nodeOf('risk').fields[1].type === 'textarea'
- },
- {
- name: '风险辨识:说明字段别名不与多选框 key 冲突',
- test: (t) =>
- !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
- !t.nodeOf('risk').fields[1].alias.includes('riskResult')
- },
- measureTableCheck,
- measureSignCheck,
- {
- name: '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
- test: (t) =>
- t
- .colOf('measure', 'measure_list', 'measure_confirm')
- .options.map((o) => o.label)
- .join(',') === '不涉及,已确认'
- },
- {
- name: '现场安全措施确认:确认人列为单行文本(源系统 input)',
- test: (t) => t.colOf('measure', 'measure_list', 'measure_confirmer').type === 'text'
- },
- {
- name: '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
- t.nodeOf('brief').fields.every((f) => f.type === 'sign')
- },
- ...opinionChecks([
- ['leader_opinion', '作业负责人意见'],
- ['unit_opinion', '所在单位意见']
- ]),
- {
- name: '消防、安全部门意见:两个意见框 + 两个签字(消防部门 / 安全部门)',
- test: (t) => {
- const node = t.nodeOf('fire_safety_opinion');
- return (
- JSON.stringify(node.fields.map((f) => f.label)) ===
- JSON.stringify([
- '消防部门意见',
- '安全部门意见',
- '消防部门签字',
- '安全部门签字'
- ]) &&
- node.fields[0].type === 'textarea' &&
- node.fields[1].type === 'textarea' &&
- node.fields[2].type === 'sign' &&
- node.fields[3].type === 'sign'
- );
- }
- },
- {
- name: '签字项标题与源系统一致(不再统一写「手写签名」)',
- test: (t) =>
- JSON.stringify(
- t.nodes.flatMap((n) =>
- n.fields.filter((f) => f.type === 'sign').map((f) => f.label)
- )
- ) ===
- JSON.stringify([
- '编制人签字',
- '确认人签字',
- '安全交底人签字',
- '接受交底人签字',
- '作业负责人签字',
- '所在单位签字',
- '消防部门签字',
- '安全部门签字',
- '审批部门签字',
- '签字'
- ])
- },
- {
- name: '同一环节内不存在重名的签字项(不会出现两个「手写签名」)',
- test: (t) =>
- t.nodes.every((n) => {
- const labels = n.fields
- .filter((f) => f.type === 'sign')
- .map((f) => f.label);
- return new Set(labels).size === labels.length;
- })
- },
- {
- name: '「消防、安全部门意见」的两个签字标题互不相同',
- test: (t) =>
- t.nodeOf('fire_safety_opinion').fields[2].label !==
- t.nodeOf('fire_safety_opinion').fields[3].label
- },
- {
- name: '意见类环节的取值 key 互不相同(源系统多处复制同一 model,不能互相覆盖)',
- test: (t) =>
- new Set(
- t.nodes
- .filter((n) => n.fields[0].type === 'textarea')
- .map((n) => n.fields[0].key)
- ).size === 5
- },
- {
- name: '审批部门意见:意见 + 签名',
- test: (t) => {
- const node = t.nodeOf('approver_opinion');
- return (
- node.fields.length === 2 &&
- node.fields[0].type === 'textarea' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
- test: (t) =>
- JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
- },
- duringRecordCheck(['时间', '记录内容', '记录人']),
- {
- name: '完工验收:意见 + 签名',
- test: (t) => {
- const node = t.nodeOf('acceptance');
- return (
- node.fields.length === 2 &&
- node.fields[0].label === '意见' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '安全措施默认清单 3 条与源系统 formData 逐条一致',
- test: (t) =>
- t.measures.length === 3 &&
- t.measures[0] === '作业前,指定交通组织方案,并已通知相关部门或单位' &&
- t.measures[1] ===
- '作业前,在断路的路口和相关道路上设置交通警示标志,在作业区域附近设置路栏、道路作业警示灯、寻向标灯交通警示设施' &&
- t.measures[2] === '夜间作业设置警示灯'
- },
- defaultRowsCheck(3)
- ],
- blind_plate: [
- {
- name: '风险辨识:结果为多选 20 项(源系统 checkbox)',
- test: (t) => {
- const f = t.nodeOf('risk').fields;
- return (
- f[0].type === 'checkbox' &&
- f[0].options.length === 20 &&
- f[0].options[0].label === '物体打击' &&
- f[0].options[19].label === '其它伤害'
- );
- }
- },
- {
- name: '风险辨识:说明字段为单行文本(源系统 input)',
- test: (t) => t.nodeOf('risk').fields[1].type === 'text'
- },
- {
- name: '风险辨识:说明字段别名不与多选框 key 冲突',
- test: (t) =>
- !t.nodeOf('risk').fields[1].alias.includes('risk_result') &&
- !t.nodeOf('risk').fields[1].alias.includes('riskResult')
- },
- measureTableCheck,
- measureSignCheck,
- {
- name: '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
- test: (t) =>
- t
- .colOf('measure', 'measure_list', 'measure_confirm')
- .options.map((o) => o.label)
- .join(',') === '不涉及,已确认'
- },
- {
- name: '现场安全措施确认:安全措施列为多行文本(源系统 textarea)',
- test: (t) => t.colOf('measure', 'measure_list', 'measure_content').type === 'textarea'
- },
- {
- name: '现场安全措施确认:确认人列为单行文本(源系统 input)',
- test: (t) => t.colOf('measure', 'measure_list', 'measure_confirmer').type === 'text'
- },
- {
- name: '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
- test: (t) =>
- JSON.stringify(t.nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
- t.nodeOf('brief').fields.every((f) => f.type === 'sign')
- },
- ...titledOpinionChecks([
- ['leader_opinion', '作业负责人意见', '作业负责人签字'],
- ['unit_opinion', '所在单位意见', '所在单位签字']
- ]),
- {
- name: '签字项标题能分辨出是谁签的(同一环节内不重名)',
- test: (t) =>
- t.nodes.every((n) => {
- const labels = n.fields
- .filter((f) => f.type === 'sign')
- .map((f) => f.label);
- return new Set(labels).size === labels.length;
- })
- },
- {
- name: '意见类环节的取值 key 互不相同(源系统多处复制同一 model,不能互相覆盖)',
- test: (t) =>
- new Set(
- t.nodes
- .filter((n) => n.fields[0].type === 'textarea')
- .map((n) => n.fields[0].key)
- ).size === 3
- },
- {
- name: '作业实施:实施开始时间 + 事中记录表(源系统只有开始时间,没有结束时间)',
- test: (t) =>
- JSON.stringify(t.nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify(['作业实施开始时间', '事中记录表'])
- },
- duringRecordCheck(['时间', '记录内容', '记录人']),
- {
- name: '事中记录表「时间」列为日期时间(源系统 datePicker)',
- test: (t) =>
- t
- .nodeOf('execute')
- .fields.find((f) => f.key === 'during_record_list')
- .columns.some((c) => c.key === 'record_time' && c.type === 'date')
- },
- {
- name: '完工验收:意见 + 签名',
- test: (t) => {
- const node = t.nodeOf('acceptance');
- return (
- node.fields.length === 2 &&
- node.fields[0].label === '意见' &&
- node.fields[1].type === 'sign'
- );
- }
- },
- {
- name: '安全措施默认清单 10 条与源系统 formData 逐条一致',
- test: (t) =>
- t.measures.length === 10 &&
- t.measures[0] === '在管道、设备上作业时,降低系统压力,作业点应为常压或微正压' &&
- t.measures[1] ===
- '在有毒介质的管道、设备上作业时,尽可能降低系统压力,作业点应为常压' &&
- t.measures[2] ===
- '在有毒介质的管道、设备上作业时,作业人员穿戴适合的防护用具' &&
- t.measures[3] ===
- '易燃易爆场所,作业人员穿防静电工作服、工作鞋;作业时使用防爆灯具和防爆工具' &&
- t.measures[4] === '易燃易爆场所,距作业地点30m内无其他动火作业' &&
- t.measures[5] ===
- '在强腐蚀性介质的管道、设备上作业时,作业人员已采取防止酸碱灼伤的措施' &&
- t.measures[6] ===
- '介质温度较高、可能造成烫伤的情况下,作业人员已采取防烫措施' &&
- t.measures[7] === '同一管道上不同时进行两处以上的盲板抽堵作业' &&
- t.measures[8] === '其它相关特殊作业已办理相应安全作业票' &&
- t.measures[9] === '作业现场四周已设警戒区'
- },
- defaultRowsCheck(10)
- ]
- };
- // 环节匹配的断言(逐类型)
- const MATCH_CHECKS = {
- fire: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_FIRE_THREETH', key: 'gas_analysis' },
- { name: '按节点名称命中', code: '现场安全措施落实', key: 'measure' },
- { name: '按别名命中', code: '气体检测', key: 'gas_analysis' },
- { name: '按平台自定义 key 反向包含命中', code: 'fire_gas_analysis', key: 'gas_analysis' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_FIRE', 'HO_PERMIT_FIRE_ONETH'] },
- { name: '空标识返回 null', empty: true }
- ],
- confined_space: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_CONFINED_SPACE_THREETH', key: 'gas_analysis' },
- { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
- { name: '按别名命中', code: '气体检测', key: 'gas_analysis' },
- { name: '按平台自定义 key 反向包含命中', code: 'cs_gas_analysis', key: 'gas_analysis' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_CONFINED_SPACE', 'HO_PERMIT_CONFINED_SPACE_ONETH'] },
- { name: '空标识返回 null', empty: true }
- ],
- height: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_HEIGHT_THREETH', key: 'measure' },
- { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
- { name: '按别名命中', code: '危险辨识', key: 'risk' },
- { name: '按平台自定义 key 反向包含命中', code: 'height_risk', key: 'risk' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_HEIGHT', 'HO_PERMIT_HEIGHT_ONETH'] },
- { name: '空标识返回 null', empty: true }
- ],
- hoisting: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_HOISTING_THREETH', key: 'measure' },
- { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
- { name: '按别名命中', code: '危险辨识', key: 'risk' },
- { name: '按平台自定义 key 反向包含命中', code: 'hoisting_risk', key: 'risk' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_HOISTING', 'HO_PERMIT_HOISTING_ONETH'] },
- { name: '空标识返回 null', empty: true }
- ],
- electricity: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_ELECTRICITY_THREETH', key: 'gas_analysis' },
- { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
- { name: '按别名命中', code: '气体检测', key: 'gas_analysis' },
- { name: '按平台自定义 key 反向包含命中', code: 'el_gas_analysis', key: 'gas_analysis' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_ELECTRICITY', 'HO_PERMIT_ELECTRICITY_ONETH'] },
- { name: '空标识返回 null', empty: true },
- { name: '「危害辨识」不会被「风险辨识」抢走', code: '危害辨识', key: 'hazard' },
- { name: '「危险辨识」归到风险辨识节点', code: '危险辨识', key: 'risk' }
- ],
- break_ground: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_BREAK_GROUND_THREETH', key: 'measure' },
- { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
- { name: '按别名命中', code: '危险辨识', key: 'risk' },
- { name: '按平台自定义 key 反向包含命中', code: 'break_ground_risk', key: 'risk' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_BREAK_GROUND', 'HO_PERMIT_BREAK_GROUND_ONETH'] },
- { name: '空标识返回 null', empty: true }
- ],
- break_road: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_BREAK_ROAD_THREETH', key: 'measure' },
- { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
- { name: '按别名命中', code: '危险辨识', key: 'risk' },
- { name: '按平台自定义 key 反向包含命中', code: 'break_road_risk', key: 'risk' },
- { name: '第七级按 code 命中消防、安全部门意见', code: 'HO_PERMIT_BREAK_ROAD_SEVENTH', key: 'fire_safety_opinion' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_BREAK_ROAD', 'HO_PERMIT_BREAK_ROAD_ONETH'] },
- { name: '空标识返回 null', empty: true }
- ],
- blind_plate: [
- { name: '按任务定义 key 精确命中', code: 'HO_PERMIT_BLIND_PLATE_THREETH', key: 'measure' },
- { name: '按节点名称命中', code: '现场安全措施确认', key: 'measure' },
- { name: '按别名命中', code: '危险辨识', key: 'risk' },
- { name: '按平台自定义 key 反向包含命中', code: 'blind_plate_risk', key: 'risk' },
- { name: '第五级按 code 命中作业负责人意见', code: 'HO_PERMIT_BLIND_PLATE_FIVETH', key: 'leader_opinion' },
- { name: '流程定义 key / 一级作业申请 key 不误命中后续环节', notMatch: ['HO_PERMIT_BLIND_PLATE', 'HO_PERMIT_BLIND_PLATE_ONETH'] },
- { name: '空标识返回 null', empty: true }
- ]
- };
- // 运行时辅助(回显 / payload / 流程变量)的断言(逐类型)
- const RUNTIME_CHECKS = {
- // 八类作业通用的 5 条
- common: [
- {
- name: '回显:按自身 key 取到已保存内容',
- test: (t) => {
- const loaded = runtime.loadNodeValues(t.nodeOf('measure'), [
- RUNTIME_SOURCE
- ]);
- return (
- loaded.measure_list.length === 1 &&
- loaded.measure_preparer_sign === '/tmp/a.png'
- );
- }
- },
- {
- name: '回显:未保存的字段为 undefined',
- test: (t) =>
- runtime.loadNodeValues(t.nodeOf('measure'), [RUNTIME_SOURCE])
- .measure_confirmer_sign === undefined
- },
- {
- name: 'payload:保留原始类型(数组不转字符串)',
- test: (t) => {
- const payload = runtime.buildNodePayload(t.nodeOf('measure'), {
- measure_list: [{ measure_content: 'x' }],
- measure_preparer_sign: '/tmp/a.png'
- });
- return Array.isArray(payload.measure_list) && payload.measure_list.length === 1;
- }
- },
- {
- name: 'payload:空值不写入',
- test: (t) => {
- const payload = runtime.buildNodePayload(t.nodeOf('measure'), {
- measure_list: [{ measure_content: 'x' }],
- measure_preparer_sign: '/tmp/a.png'
- });
- return (
- !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload)
- );
- }
- },
- {
- name: '流程变量:子表转 JSON 字符串、其余转字符串',
- test: (t) => {
- const vars = runtime.buildNodeVariables(t.nodeOf('measure'), {
- measure_list: [{ measure_content: 'x' }],
- measure_preparer_sign: '/tmp/a.png'
- });
- return (
- typeof vars.measure_list === 'string' &&
- vars.measure_preparer_sign === '/tmp/a.png'
- );
- }
- }
- ],
- fire: [
- {
- name: '流程变量:多选值用逗号连接',
- test: (t) => {
- const vars = runtime.buildNodeVariables(t.nodeOf('risk'), {
- risk_result: ['5', '8'],
- risk_result_desc: '触电、火灾'
- });
- return (
- vars.risk_result === '5,8' && vars.risk_result_desc === '触电、火灾'
- );
- }
- }
- ],
- confined_space: [],
- height: [
- {
- name: '回显:单选框的值不会串到风险辨识说明',
- test: (t) =>
- runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
- .risk_result_desc === undefined
- },
- {
- name: '安全交底:两个签字写入 payload 与流程变量',
- test: (t) => {
- const briefPayload = runtime.buildNodePayload(t.nodeOf('brief'), {
- brief_sign: '/tmp/briefer.png',
- brief_receiver_sign: '/tmp/receiver.png'
- });
- return (
- briefPayload.brief_sign === '/tmp/briefer.png' &&
- briefPayload.brief_receiver_sign === '/tmp/receiver.png' &&
- runtime.buildNodeVariables(t.nodeOf('brief'), {
- brief_sign: '/tmp/briefer.png'
- }).brief_sign === '/tmp/briefer.png'
- );
- }
- }
- ],
- hoisting: [
- {
- name: '回显:单选框的值不会串到风险辨识说明',
- test: (t) =>
- runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
- .risk_result_desc === undefined
- },
- {
- name: '安全交底:两个签字写入 payload 与流程变量',
- test: (t) => {
- const briefPayload = runtime.buildNodePayload(t.nodeOf('brief'), {
- brief_sign: '/tmp/briefer.png',
- brief_receiver_sign: '/tmp/receiver.png'
- });
- return (
- briefPayload.brief_sign === '/tmp/briefer.png' &&
- briefPayload.brief_receiver_sign === '/tmp/receiver.png' &&
- runtime.buildNodeVariables(t.nodeOf('brief'), {
- brief_sign: '/tmp/briefer.png'
- }).brief_sign === '/tmp/briefer.png'
- );
- }
- }
- ],
- electricity: [
- {
- name: '流程变量:多选值用逗号连接',
- test: (t) => {
- const riskVars = runtime.buildNodeVariables(t.nodeOf('risk'), {
- risk_result: ['5', '8'],
- risk_result_desc: '触电、火灾'
- });
- return (
- riskVars.risk_result === '5,8' &&
- riskVars.risk_result_desc === '触电、火灾'
- );
- }
- },
- {
- name: '流程变量:风险辨识与危害辨识各自独立',
- test: (t) => {
- const riskVars = runtime.buildNodeVariables(t.nodeOf('risk'), {
- risk_result: ['5', '8'],
- risk_result_desc: '触电、火灾'
- });
- const hazardVars = runtime.buildNodeVariables(t.nodeOf('hazard'), {
- hazard_result: ['5'],
- hazard_result_desc: '触电'
- });
- return (
- riskVars.risk_result === '5,8' && hazardVars.hazard_result === '5'
- );
- }
- }
- ],
- break_ground: [
- {
- name: '回显:多选框的值不会串到风险辨识说明',
- test: (t) =>
- runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
- .risk_result_desc === undefined
- },
- {
- name: '作业负责人意见:意见 + 签名写入 payload 与流程变量',
- test: (t) => {
- const opinionPayload = runtime.buildNodePayload(
- t.nodeOf('leader_opinion'),
- { leader_opinion: '同意', leader_sign: '/tmp/leader.png' }
- );
- return (
- opinionPayload.leader_opinion === '同意' &&
- opinionPayload.leader_sign === '/tmp/leader.png' &&
- runtime.buildNodeVariables(t.nodeOf('leader_opinion'), {
- leader_opinion: '同意'
- }).leader_opinion === '同意'
- );
- }
- },
- {
- name: '四个意见类环节的 payload 互不覆盖',
- test: (t) =>
- runtime.buildNodePayload(t.nodeOf('unit_opinion'), {
- unit_opinion: '单位意见'
- }).unit_opinion === '单位意见' &&
- runtime.buildNodePayload(t.nodeOf('approver_opinion'), {
- approver_opinion: '审批意见'
- }).approver_opinion === '审批意见'
- }
- ],
- break_road: [
- {
- name: '回显:多选框的值不会串到风险辨识说明',
- test: (t) =>
- runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
- .risk_result_desc === undefined
- },
- {
- name: '作业负责人意见:意见 + 签名写入 payload 与流程变量',
- test: (t) => {
- const opinionPayload = runtime.buildNodePayload(
- t.nodeOf('leader_opinion'),
- { leader_opinion: '同意', leader_sign: '/tmp/leader.png' }
- );
- return (
- opinionPayload.leader_opinion === '同意' &&
- opinionPayload.leader_sign === '/tmp/leader.png' &&
- runtime.buildNodeVariables(t.nodeOf('leader_opinion'), {
- leader_opinion: '同意'
- }).leader_opinion === '同意'
- );
- }
- },
- {
- name: '消防、安全部门意见:两处意见互不覆盖',
- test: (t) => {
- const payload = runtime.buildNodePayload(
- t.nodeOf('fire_safety_opinion'),
- {
- fire_dept_opinion: '消防意见',
- safety_dept_opinion: '安全意见'
- }
- );
- return (
- payload.fire_dept_opinion === '消防意见' &&
- payload.safety_dept_opinion === '安全意见'
- );
- }
- },
- {
- name: '所在单位意见 / 审批部门意见的 payload 互不覆盖',
- test: (t) =>
- runtime.buildNodePayload(t.nodeOf('unit_opinion'), {
- unit_opinion: '单位意见'
- }).unit_opinion === '单位意见' &&
- runtime.buildNodePayload(t.nodeOf('approver_opinion'), {
- approver_opinion: '审批意见'
- }).approver_opinion === '审批意见'
- }
- ],
- blind_plate: [
- {
- name: '回显:多选框的值不会串到风险辨识说明',
- test: (t) =>
- runtime.loadNodeValues(t.nodeOf('risk'), [{ risk_result: '9' }])
- .risk_result_desc === undefined
- },
- {
- name: '作业负责人意见:意见 + 签名写入 payload 与流程变量',
- test: (t) => {
- const opinionPayload = runtime.buildNodePayload(
- t.nodeOf('leader_opinion'),
- { leader_opinion: '同意', leader_sign: '/tmp/leader.png' }
- );
- return (
- opinionPayload.leader_opinion === '同意' &&
- opinionPayload.leader_sign === '/tmp/leader.png' &&
- runtime.buildNodeVariables(t.nodeOf('leader_opinion'), {
- leader_opinion: '同意'
- }).leader_opinion === '同意'
- );
- }
- },
- {
- name: '所在单位意见 / 完工验收的 payload 互不覆盖',
- test: (t) =>
- runtime.buildNodePayload(t.nodeOf('unit_opinion'), {
- unit_opinion: '单位意见'
- }).unit_opinion === '单位意见' &&
- runtime.buildNodePayload(t.nodeOf('acceptance'), {
- acceptance_opinion: '验收意见'
- }).acceptance_opinion === '验收意见'
- }
- ]
- };
- // 源系统「现场安全措施确认」节点 formData 的默认 20 条(吊装作业),逐条比对
- const HOISTING_SOURCE_MEASURES = [
- '一、二级吊装作业已编制吊装作业方案,已经审查批准;吊装物体形状复杂、刚度小,长径比大、精密贵重,作业条件特殊的三级吊装作业,已编制吊装作业方案,已经审查批准',
- '吊装场所如有含危险物料的设备、管道时,应制定详细吊装方案,并对设备、管道采取有效防护措施,必要时停车,放空物料,置换后再进行吊装作业',
- '作业人员已按规定佩戴个体防护装备',
- '已对起重吊装设备、钢丝绳、揽风绳、链条、吊钩等各种机具进行检查,安全可靠',
- '已明确各自分工',
- '坚守岗位,并统一规定联络信号',
- '将建筑物、构筑物作为锚点,应经所属单位工程管理部门审查校算并批准',
- '吊装绳索、揽风绳、拖拉绳等不应与带电线路接触,并保持安全距离',
- '不应利用管道、管架、电杆、机电设备等作吊装锚点',
- '吊物捆扎坚固,未见绳打结、绳不齐现象,棱角吊物已采取衬垫措施',
- '起重机安全装置灵活好用',
- '吊装作业人员持有有效的法定资格证书',
- '地下通信电(光)缆、局域网络电(光)缆,排水沟的益板,承重吊装机械的负重量已确认,保护措施已落实',
- '起吊物的质量(t)经确认,在吊装机械的承重范围内',
- '在吊装高度的管线、电缆桥架已做好防护措施',
- '作业现场围栏、警戒线、警告牌、夜同警示灯已按要求设置',
- '作业高度和转臂范围内无架空线路',
- '在爆炸危险场所内的作业,机动车排气管已装阻火器',
- '露天作业,环境风力满足作业安全要求',
- '其他相关特殊作业已办理相应安全作业票'
- ];
- // 运行时辅助断言里用的固定数据源
- const RUNTIME_SOURCE = {
- measure_list: [
- { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' }
- ],
- measure_preparer_sign: '/tmp/a.png'
- };
- // ---------------------------------------------------------------------------
- // 四、组件模板 / 脚本体检工具
- // ---------------------------------------------------------------------------
- /**
- * 收集组件脚本里已定义的成员名(props / data / computed / methods)
- * @param {string} script script 源码
- * @returns {Set<string>} 成员名集合
- */
- function collectComponentMembers(script) {
- const names = new Set();
- const ast = parser.parse(script, {
- sourceType: 'module',
- plugins: [
- 'optionalChaining',
- 'objectRestSpread',
- 'nullishCoalescingOperator'
- ]
- });
- let component = null;
- for (const node of ast.program.body) {
- if (node.type === 'ExportDefaultDeclaration') {
- component = node.declaration;
- }
- }
- const collectObjectKeys = (obj) => {
- ((obj && obj.properties) || []).forEach((prop) => {
- if (!prop.key) {
- return;
- }
- names.add(prop.key.name || prop.key.value);
- });
- };
- ((component && component.properties) || []).forEach((prop) => {
- if (!prop.key) {
- return;
- }
- const key = prop.key.name;
- const value = prop.value;
- if (key === 'props' || key === 'methods' || key === 'computed') {
- collectObjectKeys(value);
- return;
- }
- if (key === 'data') {
- // data() { return { ... } }
- const body = value && value.body;
- if (!body) {
- return;
- }
- const ret = body.body.find(
- (stmt) => stmt.type === 'ReturnStatement' && stmt.argument
- );
- if (ret) {
- collectObjectKeys(ret.argument);
- }
- }
- });
- return names;
- }
- /**
- * 取模板里被调用的成员名
- *
- * 注意:Vue 2 编译后的 render 用 with(this) 解析标识符,
- * 方法调用编译成裸函数名(如 getRows(field)),不带 vm. 前缀,
- * 因此这里直接扫描模板表达式,而不是扫描编译产物。
- *
- * @param {string} template 模板源码
- * @returns {object} { calls, errors }
- */
- function collectTemplateCalls(template) {
- const calls = new Set();
- // 1) 插值 {{ expr }}
- // 2) 指令 / 事件绑定属性值: :xxx="expr"、v-xxx="expr"、v-xxx:arg="expr"、@xxx="expr"
- const attrRe = /(?:[:@][\w.-]+|v-[\w-]+(?::[\w.-]+)?)\s*=\s*"([^"]*)"/g;
- const interpRe = /\{\{([\s\S]*?)\}\}/g;
- const exprs = [];
- let m;
- while ((m = attrRe.exec(template)) !== null) {
- exprs.push(m[1]);
- }
- while ((m = interpRe.exec(template)) !== null) {
- exprs.push(m[1]);
- }
- // 全局函数与 Vue 内部函数,不属于组件成员
- const globals = new Set([
- 'String',
- 'Number',
- 'Boolean',
- 'Array',
- 'Object',
- 'JSON',
- 'Math',
- 'Date',
- 'parseInt',
- 'parseFloat',
- 'isNaN',
- 'require'
- ]);
- // 标识符后紧跟 "(" 即为函数调用;排除属性访问(xxx.method())与内部变量
- const callRe = /(^|[^.\w$])([A-Za-z_$][\w$]*)\s*\(/g;
- exprs.forEach((expr) => {
- let hit;
- callRe.lastIndex = 0;
- while ((hit = callRe.exec(expr)) !== null) {
- const name = hit[2];
- if (name.startsWith('$') || name.startsWith('_') || globals.has(name)) {
- continue;
- }
- calls.add(name);
- }
- });
- return { calls, errors: compiler.compile(template).errors || [] };
- }
- // ---------------------------------------------------------------------------
- // 五、逐节校验
- // ---------------------------------------------------------------------------
- console.log('\n[1] 公共模块(flowRuntime / flowMatcher / flowFields)');
- ['flowRuntime.js', 'flowMatcher.js', 'flowFields.js'].forEach((file) => {
- check(file + ' 存在', fs.existsSync(path.join(SRC, file)));
- });
- const RUNTIME_EXPORTS = [
- 'getCurrentTaskInfo',
- 'loadProcessFormVariables',
- 'getFieldKeys',
- 'loadNodeValues',
- 'buildNodeVariables',
- 'buildNodePayload',
- 'mergeExtension',
- 'buildTicketUpdateParams',
- 'parseJsonObject'
- ];
- RUNTIME_EXPORTS.forEach((fn) => {
- check('flowRuntime.js 导出 ' + fn, typeof runtime[fn] === 'function');
- });
- check('flowMatcher.js 导出 matchFlowNode', typeof matcher.matchFlowNode === 'function');
- check('flowFields.js 导出 getFieldKeys', typeof fieldUtils.getFieldKeys === 'function');
- check(
- 'flowRuntime.js 再导出的 getFieldKeys 与 flowFields.js 是同一实现',
- runtime.getFieldKeys === fieldUtils.getFieldKeys
- );
- console.log('\n[2] 八个作业类型的节点定义文件');
- TYPES.forEach((type) => {
- console.log(' -- ' + type.label + '作业(' + type.module + '.js)');
- check(
- type.module + '.js 存在',
- fs.existsSync(path.join(SRC, type.module + '.js'))
- );
- check(
- type.module + '.js 导出 ' + type.nodesConst,
- Array.isArray(type.mod[type.nodesConst]) && type.mod[type.nodesConst].length > 0
- );
- check(
- type.module + '.js 导出 ' + type.matchFn,
- typeof type.mod[type.matchFn] === 'function'
- );
- check(
- type.module + '.js 再导出了 getFieldKeys',
- typeof type.mod.getFieldKeys === 'function'
- );
- // 包装函数确实委托给通用匹配器:同一组候选标识下两者结果一致
- const candidates = []
- .concat(type.nodes.map((n) => n.code))
- .concat(type.nodes.map((n) => n.name))
- .concat(type.nodes.map((n) => n.key))
- .concat([type.flowCode, type.flowCode + '_ONETH', '', null, undefined]);
- const delegated = candidates.every((c) => {
- const a = matcher.matchFlowNode(type.nodes, c);
- const b = type.match(c);
- return (a && a.key) === (b && b.key);
- });
- check(
- type.module + '.js 的 ' + type.matchFn + ' 与 matchFlowNode 对同一组候选标识结果一致',
- delegated
- );
- // 作业类型之间互不污染:节点定义文件只依赖公共模块,不引用其它作业类型的文件
- const ownSource = fs.readFileSync(path.join(SRC, type.module + '.js'), 'utf8');
- const otherTypes = TYPES.filter((t) => t.module !== type.module);
- const foreignImports = otherTypes.filter(
- (t) => ownSource.indexOf("'./" + t.module + "'") >= 0
- );
- check(
- type.module + '.js 不引用其它作业类型的节点定义文件',
- foreignImports.length === 0,
- foreignImports.map((t) => t.module)
- );
- const keys = type.nodes.map((n) => n.key);
- const codes = type.nodes.map((n) => n.code);
- const names = type.nodes.map((n) => n.name);
- check(type.nodesConst + ' 的 key 唯一', new Set(keys).size === keys.length, keys);
- check(type.nodesConst + ' 的 code 唯一', new Set(codes).size === codes.length, codes);
- check(type.nodesConst + ' 的 name 唯一', new Set(names).size === names.length, names);
- check(
- type.nodesConst + ' 的 code 均以 ' + type.codePrefix + ' 开头',
- codes.every((c) => c.indexOf(type.codePrefix) === 0),
- codes.filter((c) => c.indexOf(type.codePrefix) !== 0)
- );
- });
- // 动火作业沿用自己的实现(fireFlowTask.js 已由公共的 flowRuntime.js 取代)
- [
- 'fireFlowNodes.js',
- 'components/fireTicketDetail.vue',
- 'components/flowNodeForm.vue'
- ].forEach((file) => {
- check('动火作业的既有实现文件 ' + file + ' 仍然存在', fs.existsSync(path.join(SRC, file)));
- });
- TYPES.forEach((type) => {
- console.log(
- '\n[3] ' + type.label + '作业环节(源系统 ' + type.flowCode + ' 共 ' + (type.count + 1) + ' 节点,1 级为作业申请模板)'
- );
- check('环节数量为 ' + type.count, type.nodes.length === type.count, {
- len: type.nodes.length
- });
- type.expected.forEach(([code, name], i) => {
- const node = type.nodes[i];
- check(
- '第' + (i + 2) + '级 ' + code + ' / ' + name,
- node.code === code && node.name === name,
- { code: node.code, name: node.name }
- );
- });
- console.log('\n[4] ' + type.label + '作业:与源系统 formJson 一致性');
- FORM_CHECKS[type.key].forEach((spec) => {
- check(
- spec.name,
- spec.test(type),
- spec.extra ? spec.extra(type) : undefined
- );
- });
- console.log('\n[5] ' + type.label + '作业:环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
- MATCH_CHECKS[type.key].forEach((spec) => {
- if (spec.notMatch) {
- const hit = spec.notMatch.map((c) => type.match(c));
- check(
- spec.name,
- hit.every((m) => m === null),
- hit.map((m) => (m && m.key) || null)
- );
- return;
- }
- if (spec.empty) {
- check(spec.name, type.match('', null) === null);
- return;
- }
- const m = type.match(spec.code);
- check(spec.name, m && m.key === spec.key, { key: m && m.key });
- });
- console.log('\n[6] ' + type.label + '作业:运行时辅助(回显 / payload / 流程变量)');
- RUNTIME_CHECKS.common.concat(RUNTIME_CHECKS[type.key] || []).forEach((spec) => {
- check(spec.name, spec.test(type));
- });
- });
- console.log('\n[7] detail.vue:由 FLOW_TYPES 配置表驱动(七类作业各自一个环节组件)');
- const detailSrc = fs.readFileSync(path.join(SRC, 'detail.vue'), 'utf8');
- const templatesSrc = fs.readFileSync(path.join(SRC, 'templates.js'), 'utf8');
- // 作业类型 -> 环节组件(xxxFlowNodes.js 对应 xxxFlow.vue)
- function componentFileOf(t) {
- return t.module.replace(/FlowNodes$/, 'Flow');
- }
- function componentIdentOf(t) {
- const file = componentFileOf(t);
- return file.charAt(0).toUpperCase() + file.slice(1);
- }
- const GENERIC_TYPES = TYPES.filter((t) => t.templateKey);
- const flowTypesStart = detailSrc.indexOf('const FLOW_TYPES = [');
- const flowTypesEnd = detailSrc.indexOf('];', flowTypesStart);
- const flowTypesBlock =
- flowTypesStart >= 0 ? detailSrc.slice(flowTypesStart, flowTypesEnd) : '';
- check('detail.vue 源码包含 FLOW_TYPES 配置表', flowTypesStart >= 0 && flowTypesEnd > flowTypesStart);
- GENERIC_TYPES.forEach((t) => {
- check(
- 'FLOW_TYPES 登记了' + t.label + '作业(key = ' + t.templateKey + ')',
- new RegExp("key:\\s*'" + t.templateKey + "'").test(flowTypesBlock)
- );
- });
- check(
- 'FLOW_TYPES 的每一项都绑定了自己的节点表、匹配函数与环节组件',
- GENERIC_TYPES.every(
- (t) =>
- flowTypesBlock.indexOf(t.nodesConst) >= 0 &&
- flowTypesBlock.indexOf(t.matchFn) >= 0 &&
- flowTypesBlock.indexOf(componentIdentOf(t)) >= 0
- )
- );
- // component 字段的值必须与组件名一致,否则 <component :is> 渲染不出来
- GENERIC_TYPES.forEach((t) => {
- const expect = componentIdentOf(t);
- const m = flowTypesBlock.match(
- new RegExp("key:\\s*'" + t.templateKey + "'[\\s\\S]{0,400}?component:\\s*'([^']+)'")
- );
- check(
- 'FLOW_TYPES[' + t.templateKey + '].component === ' + expect,
- !!m && m[1] === expect,
- m ? m[1] : null
- );
- });
- check(
- '动火作业(fire)不在 FLOW_TYPES 内(沿用自己的实现)',
- !/key:\s*'fire'/.test(flowTypesBlock)
- );
- check(
- 'detail.vue 用 currentFlowType 计算属性定位作业类型',
- /currentFlowType\(\)\s*\{[\s\S]{0,400}FLOW_TYPES\.find/.test(detailSrc)
- );
- check(
- 'detail.vue 用 currentFlowNode 计算属性定位当前处理环节',
- /currentFlowNode\(\)\s*\{[\s\S]{0,400}currentFlowType\.match/.test(detailSrc)
- );
- // 只统计 <template> 块内的标签,注释里出现的 <component 不算
- const detailTemplate = compiler.parseComponent(detailSrc).template.content;
- const dynamicTags = detailTemplate.match(/<component\b/g) || [];
- check(
- 'detail.vue 的模板里只有一个动态组件标签(<component :is>)',
- dynamicTags.length === 1,
- { count: dynamicTags.length }
- );
- check(
- 'detail.vue 用 <component :is="currentFlowType.component"> 渲染对应作业类型的环节组件',
- /:is="currentFlowType\.component"/.test(detailTemplate)
- );
- check(
- 'detail.vue 注册了七个作业类型的环节组件',
- GENERIC_TYPES.every((t) =>
- new RegExp(
- "import\\s+" +
- componentIdentOf(t) +
- "\\s+from\\s+'\\./components\\/" +
- componentFileOf(t) +
- "\\.vue'"
- ).test(detailSrc)
- )
- );
- check(
- 'detail.vue 的取数 / 保存方法对七类作业统一走同一个动态组件',
- /getFlowTableValue\(\)/.test(detailSrc) &&
- /saveFlow\(data\)/.test(detailSrc) &&
- /validateFlowNodeForm\(\)/.test(detailSrc) &&
- /\$refs\.flowView/.test(detailSrc)
- );
- check(
- 'detail.vue 的「后续环节」分发已收敛到 FLOW_TYPES 配置表(旧的逐类型分发逻辑已移除)',
- flowTypesStart >= 0 &&
- !/isConfinedSpaceWork|isHeightWork|isHoistingWork|isElectricityWork|isBreakGroundWork|isBreakRoadWork|isBlindPlateWork/.test(
- detailSrc
- )
- );
- check(
- 'detail.vue:按作业类型分发到各自的后续环节组件',
- /currentFlowType\(\)[\s\S]{0,400}FLOW_TYPES\.find/.test(detailSrc) &&
- /currentFlowNode\(\)[\s\S]{0,400}currentFlowType\.match/.test(detailSrc)
- );
- check(
- 'detail.vue 的默认 tab 是「作业票信息」(不再自动切到「后续环节」)',
- /activeTab:\s*'info'/.test(detailSrc) && !/switchToFlowTab/.test(detailSrc)
- );
- check(
- 'detail.vue:作业票模板已包含临时用电作业(templates.js key = electricity)',
- /key:\s*'electricity'/.test(templatesSrc)
- );
- check(
- 'detail.vue:作业票模板已包含吊装作业(templates.js key = hoisting)',
- /key:\s*'hoisting'/.test(templatesSrc)
- );
- check(
- 'detail.vue 不再引用 xxxFlowTask(各作业类型的运行时辅助已统一到 flowRuntime.js)',
- !/FlowTask/.test(detailSrc)
- );
- check(
- 'detail.vue 引用了公共运行时 flowRuntime.js',
- /from '\.\/flowRuntime'/.test(detailSrc)
- );
- const flowTaskFiles = fs
- .readdirSync(SRC)
- .filter((f) => /FlowTask\.js$/.test(f));
- check(
- '八份 xxxFlowTask.js 已收敛为公共的 flowRuntime.js(不再有作业类型专属的运行时辅助文件)',
- flowTaskFiles.length === 0,
- flowTaskFiles
- );
- GENERIC_TYPES.forEach((t) => {
- check(
- t.label + '作业的环节组件存在(components/' + componentFileOf(t) + '.vue)',
- fs.existsSync(path.join(SRC, 'components', componentFileOf(t) + '.vue'))
- );
- });
- check(
- '通用的 flowNodeView.vue 已下线(环节组件按作业类型各自独立实现)',
- !fs.existsSync(path.join(SRC, 'components', 'flowNodeView.vue'))
- );
- console.log('\n[8] 七个环节组件:接口一致性与模板/脚本一致性');
- // 环节组件必须提供的方法(detail.vue 依赖 validate / getPayload / getVariables)
- const FLOW_VIEW_METHODS = [
- 'validate',
- 'getPayload',
- 'getVariables',
- 'buildForm',
- 'toArray',
- 'update',
- 'updateCell',
- 'addRow',
- 'removeRow',
- 'getRows',
- 'getCodes',
- 'getConfirmColumn',
- 'getConfirmValue',
- 'confirmAll',
- 'buildRules',
- 'buildFieldRules',
- 'validateTableRows',
- 'getRawValue',
- 'getViewCodes',
- 'getViewText',
- 'getViewRows',
- 'getCellText',
- 'getPreviewUrl',
- 'handleImgError',
- 'isCurrent',
- 'nodeDone'
- ];
- const FLOW_VIEW_PROPS = [
- 'node',
- 'nodes',
- 'ticket',
- 'extension',
- 'variables',
- 'taskLabel'
- ];
- GENERIC_TYPES.forEach((t) => {
- const file = componentFileOf(t);
- const source = fs.readFileSync(
- path.join(SRC, 'components', file + '.vue'),
- 'utf8'
- );
- const sfc = compiler.parseComponent(source);
- const templateCheck = collectTemplateCalls(sfc.template.content);
- const members = collectComponentMembers(sfc.script.content);
- const missing = [...templateCheck.calls].filter((name) => !members.has(name));
- check(file + '.vue 模板编译无错误', templateCheck.errors.length === 0, templateCheck.errors);
- check(
- file + '.vue 模板调用的 ' + templateCheck.calls.size + ' 个方法均已定义',
- missing.length === 0,
- missing
- );
- check(
- file + '.vue 对外提供 validate / getPayload / getVariables',
- /validate\(\)\s*\{/.test(sfc.script.content) &&
- /getPayload\(\)\s*\{/.test(sfc.script.content) &&
- /getVariables\(\)\s*\{/.test(sfc.script.content)
- );
- check(
- file + '.vue 定义了约定的全部方法(' + FLOW_VIEW_METHODS.length + ' 个)',
- FLOW_VIEW_METHODS.every((name) => members.has(name)),
- FLOW_VIEW_METHODS.filter((name) => !members.has(name))
- );
- check(
- file + '.vue 的 props 声明了 node / nodes / ticket / extension / variables / taskLabel',
- FLOW_VIEW_PROPS.every((name) => members.has(name)),
- FLOW_VIEW_PROPS.filter((name) => !members.has(name))
- );
- check(
- file + '.vue 的 computed 声明了 hasFields / displayNodes',
- /hasFields\(\)\s*\{/.test(sfc.script.content) &&
- /displayNodes\(\)\s*\{/.test(sfc.script.content)
- );
- check(
- file + '.vue 源码包含 getConfirmColumn / getConfirmValue(子表「全部确认」)',
- /getConfirmColumn\(field\)\s*\{/.test(sfc.script.content) &&
- /getConfirmValue\(column\)\s*\{/.test(sfc.script.content)
- );
- check(
- file + '.vue 源码包含子表日期列渲染分支(column.type === "date")',
- /column\.type === 'date'/.test(sfc.template.content)
- );
- check(
- file + '.vue 模板确实调用了 getConfirmColumn / confirmAll(子表「全部确认」按钮)',
- templateCheck.calls.has('getConfirmColumn') &&
- templateCheck.calls.has('confirmAll')
- );
- check(
- file + '.vue 无表单项的环节给出空态提示(hasFields)',
- /node-empty-tip/.test(sfc.template.content)
- );
- check(
- file + '.vue 的运行时辅助来自公共模块 flowRuntime.js',
- /from '\.\.\/flowRuntime'/.test(sfc.script.content)
- );
- check(
- file + '.vue 的节点 schema 来自本作业类型的 ' + t.module + '.js',
- new RegExp("from '\\.\\./" + t.module + "'").test(sfc.script.content)
- );
- check(
- file + '.vue 未识别到环节时展示当前任务名 / key(便于排查流程模型配置)',
- /taskLabel/.test(source)
- );
- check(
- file + '.vue 沿用同一套环节区块类名(scoped,不影响其它组件)',
- /current-node-box/.test(source) &&
- /all-nodes-box/.test(source) &&
- /node-form-header/.test(source)
- );
- });
- check(
- '作业实施:事中记录表「时间」列类型为 date(动土 / 断路)',
- TYPES.filter(
- (t) => t.templateKey === 'break_ground' || t.templateKey === 'break_road'
- ).every((t) =>
- t
- .nodeOf('execute')
- .fields.find((f) => f.key === 'during_record_list')
- .columns.some((c) => c.key === 'record_time' && c.type === 'date')
- )
- );
- check(
- '作业实施:事中记录表「日期时间」列类型为 date(高处 / 吊装)',
- TYPES.filter(
- (t) => t.templateKey === 'height' || t.templateKey === 'hoisting'
- ).every((t) =>
- t
- .nodeOf('execute')
- .fields.find((f) => f.key === 'during_record_list')
- .columns.some((c) => c.key === 'record_time' && c.type === 'date')
- )
- );
- console.log('\n[9] 时间类字段:必须使用日期时间选择器(不能用输入框)');
- // 可编辑态的环节组件:七类作业的 *Flow.vue + 动火的 flowNodeForm.vue
- const EDITABLE_COMPONENTS = GENERIC_TYPES.map((t) => componentFileOf(t)).concat([
- 'flowNodeForm'
- ]);
- EDITABLE_COMPONENTS.forEach((file) => {
- const source = fs.readFileSync(
- path.join(SRC, 'components', file + '.vue'),
- 'utf8'
- );
- const sfc = compiler.parseComponent(source);
- check(
- file + '.vue 的表单项按 field.type === "date" 渲染为 el-date-picker',
- /v-else-if="field\.type === 'date'"/.test(sfc.template.content) &&
- /<el-date-picker[\s\S]{0,240}field\.type === 'date'/.test(sfc.template.content)
- );
- check(
- file + '.vue 的子表列按 column.type === "date" 渲染为 el-date-picker',
- /v-else-if="column\.type === 'date'"/.test(sfc.template.content) &&
- /<el-date-picker[\s\S]{0,240}column\.type === 'date'/.test(sfc.template.content)
- );
- });
- // schema 层:label 含「时间/日期」的字段与子表列,type 必须是 date
- const TIME_LABEL = /时间|日期/;
- TYPES.forEach((t) => {
- const bad = [];
- t.nodes.forEach((node) => {
- (node.fields || []).forEach((field) => {
- if (field.type === 'table') {
- (field.columns || []).forEach((col) => {
- if (TIME_LABEL.test(col.label || '') && col.type !== 'date') {
- bad.push(
- node.key + ' / ' + field.key + ' / 列「' + col.label + '」type=' + col.type
- );
- }
- });
- } else if (
- TIME_LABEL.test(field.label || '') &&
- field.type !== 'date' &&
- field.type !== 'table'
- ) {
- bad.push(
- node.key + ' / ' + field.key + ' / 「' + field.label + '」type=' + field.type
- );
- }
- });
- });
- check(
- t.module + ':所有含「时间/日期」的字段与子表列类型都是 date',
- bad.length === 0,
- bad
- );
- });
- console.log('\n[10] 作业票内容(基本信息)的手写签名能正常显示');
- // 作业票内容里存的是签名文件存储路径(main/xxx/signature_1.png),
- // 是相对路径,直接当 img src 用会被浏览器按页面地址解析,必然加载失败,
- // 必须拼上文件服务地址(@/utils/file 的 getFilePreviewUrl)才能显示。
- const tcvPath = path.join(SRC, 'components/ticketContentView.vue');
- const tcvSource = fs.readFileSync(tcvPath, 'utf8');
- const tcv = compiler.parseComponent(tcvSource);
- const tcvCheck = collectTemplateCalls(tcv.template.content);
- const tcvMembers = collectComponentMembers(tcv.script.content);
- check('ticketContentView.vue 模板编译无错误', tcvCheck.errors.length === 0, tcvCheck.errors);
- const tcvMissing = [...tcvCheck.calls].filter((name) => !tcvMembers.has(name));
- check(
- `ticketContentView.vue 模板调用的 ${tcvCheck.calls.size} 个方法均已定义`,
- tcvMissing.length === 0,
- tcvMissing
- );
- check(
- '签名图片地址走 getPreviewUrl(拼文件服务地址),不是把存储路径直接当 src',
- /:src="getPreviewUrl\(getValue\(field\)\)"/.test(tcv.template.content) &&
- !/:src="getValue\(field\)"/.test(tcv.template.content)
- );
- check(
- 'ticketContentView.vue 引入了 @/utils/file 的 getFilePreviewUrl',
- /import \{ getFilePreviewUrl as buildFilePreviewUrl \} from '@\/utils\/file'/.test(
- tcv.script.content
- ) &&
- /getPreviewUrl\(path\)\s*\{\s*return buildFilePreviewUrl\(path\)/.test(
- tcv.script.content
- )
- );
- check(
- '签名图片加载失败时退回显示原始值(imgErrors + handleImgError)',
- /imgErrors/.test(tcv.script.content) &&
- /handleImgError\(field\)/.test(tcv.script.content) &&
- /@error="handleImgError\(field\)"/.test(tcv.template.content)
- );
- check(
- 'isImage 兼容完整地址与 data/blob 地址(不只是图片扩展名)',
- /\^\(https\?:\|data:\|blob:\)/i.test(tcv.script.content) &&
- /svg/.test(tcv.script.content)
- );
- const blindPlateBlock = templatesSrc.slice(templatesSrc.indexOf("key: 'blind_plate'"));
- // 盲板抽堵作业票基本信息里有「位置图编制人签字」字段(type: sign)
- check(
- '盲板抽堵作业票基本信息里有「位置图编制人签字」字段(type: sign)',
- /id: 'plate_drafter'/.test(blindPlateBlock) &&
- /label: '位置图编制人签字'/.test(blindPlateBlock) &&
- /type: 'sign'/.test(blindPlateBlock)
- );
- check(
- '盲板抽堵作业票基本信息里有「盲板位置图」附件字段(type: file)',
- /id: 'plate_drawing'/.test(blindPlateBlock) &&
- /label: '盲板位置图'/.test(blindPlateBlock) &&
- /type: 'file'/.test(blindPlateBlock)
- );
- console.log('\n[11] 作业类型名称匹配:全等优先 + 包含兜底');
- const workTypes = require(path.join(OUT, 'workTypes.js'));
- const templatesMod = require(path.join(OUT, 'templates.js'));
- check(
- 'workTypes.js 导出 findByWorkTypeName',
- typeof workTypes.findByWorkTypeName === 'function'
- );
- check(
- 'workTypes.js 导出 isSameWorkTypeName',
- typeof workTypes.isSameWorkTypeName === 'function'
- );
- check(
- 'workTypes.js 导出 matchBuiltInWorkType',
- typeof workTypes.matchBuiltInWorkType === 'function'
- );
- const findByWorkTypeName = workTypes.findByWorkTypeName;
- const matchBuiltInWorkType = workTypes.matchBuiltInWorkType;
- // 第一轮:名称完全相等
- [
- '动火作业',
- '受限空间作业',
- '高处作业',
- '吊装作业',
- '临时用电作业',
- '动土作业',
- '断路作业',
- '盲板抽堵作业'
- ].forEach((dictName) => {
- const hit = matchBuiltInWorkType('', dictName);
- check(
- '字典值「' + dictName + '」按名称全等命中内置同名类型',
- !!hit && hit.name === dictName,
- hit ? hit.name : null
- );
- });
- // 第二轮:包含关系兜底
- [
- ['动火', '动火作业'],
- ['受限空间', '受限空间作业'],
- ['高处', '高处作业'],
- ['吊装', '吊装作业'],
- ['临时用电', '临时用电作业'],
- ['动土', '动土作业'],
- ['断路', '断路作业'],
- ['盲板抽堵', '盲板抽堵作业']
- ].forEach(([dictName, expect]) => {
- const hit = matchBuiltInWorkType('', dictName);
- check(
- '字典值「' + dictName + '」按包含关系兜底命中内置「' + expect + '」',
- !!hit && hit.name === expect,
- hit ? hit.name : null
- );
- });
- // 全等优先:候选表里存在「更靠前、但只是包含关系」的条目时,全等项仍然胜出
- const exactFirst = findByWorkTypeName(
- [
- { name: '登高作业', tag: 'partial' },
- { name: '高处作业', tag: 'exact' }
- ],
- '高处作业',
- (item) => [item.name]
- );
- check(
- '全等优先:候选表里「登高作业」排在前面时,「高处作业」仍命中全等项',
- !!exactFirst && exactFirst.tag === 'exact'
- );
- // 包含兜底取最具体:多条都能包含时,选名称最长的那条
- const mostSpecific = findByWorkTypeName(
- [
- { name: '动火作业', tag: 'short' },
- { name: '动火特殊作业', tag: 'long' }
- ],
- '特殊作业',
- (item) => [item.name]
- );
- check(
- '包含兜底取最具体:多条都能包含时选名称最长的那条',
- !!mostSpecific && mostSpecific.tag === 'long'
- );
- // 泛词不参与匹配
- ['作业', '类型', '其他', '其它', '作业票', '其它作业'].forEach((generic) => {
- check(
- '泛词「' + generic + '」不命中任何内置作业类型',
- matchBuiltInWorkType('', generic) === null
- );
- });
- // 编码兜底:内置字典码 / 源系统编码
- const byBuiltInCode = matchBuiltInWorkType('3', '');
- check(
- '按内置字典码兜底:workType=3 命中高处作业',
- !!byBuiltInCode && byBuiltInCode.name === '高处作业'
- );
- const bySourceCode = matchBuiltInWorkType('HO_PERMIT_HEIGHT', '');
- check(
- '按源系统编码兜底:workType=HO_PERMIT_HEIGHT 命中高处作业',
- !!bySourceCode && bySourceCode.name === '高处作业'
- );
- const nameFirst = matchBuiltInWorkType('3', '动火作业');
- check(
- '名称优先于编码:workType=3 + 名称「动火作业」命中动火作业',
- !!nameFirst && nameFirst.name === '动火作业'
- );
- check(
- '名称与编码都取不到时返回 null',
- matchBuiltInWorkType('ZZZ', '不存在的作业类型') === null
- );
- // 作业票模板匹配:按名称 / 按源系统编码
- templatesMod.WORK_TICKET_TEMPLATES.forEach((tpl) => {
- const name = (tpl.workTypeNames || [])[0];
- const byName = templatesMod.getWorkTicketTemplate('', name);
- check(
- '作业票模板按名称「' + name + '」命中模板 ' + tpl.key,
- !!byName && byName.key === tpl.key,
- byName ? byName.key : null
- );
- const bySource = templatesMod.getWorkTicketTemplate(tpl.sourceCode, '');
- check(
- '作业票模板按源系统编码 ' + tpl.sourceCode + ' 命中模板 ' + tpl.key,
- !!bySource && bySource.key === tpl.key,
- bySource ? bySource.key : null
- );
- });
- check(
- '作业票模板匹配不到时返回 null',
- templatesMod.getWorkTicketTemplate('ZZZ', '不存在的作业类型') === null
- );
- console.log(
- '\n' +
- (failed
- ? 'FAILED: ' + failed + ' 项'
- : 'ALL PASSED') +
- '(共 ' +
- total +
- ' 条断言)'
- );
- process.exit(failed ? 1 : 0);
|