| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- /**
- * 断路作业票审批流校验
- *
- * 用法:node scripts/check-break-road-work-flow-nodes.js
- *
- * 校验内容:
- * 1) 动火、受限空间、高处作业、吊装作业、临时用电作业、动土作业的既有实现文件未被改动
- * (与 HEAD 完全一致)
- * 2) detail.vue 只做「新增」:既有作业类型的分发逻辑一行未删
- * 3) 断路作业 9 个后续环节的 code / name 与源系统一致
- * 4) 各环节的表单设计与源系统 formJson 一致(字段类型 / 列 / 必填项)
- * 5) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
- * 6) 默认安全措施清单与源系统 formData 逐条一致
- * 7) 运行时辅助(回显 / payload / 流程变量)的取值口径正确
- * 8) 组件模板与脚本一致:模板里调用的方法必须都已在组件中定义
- * (漏定义会在渲染到该分支时抛 "is not a function",整个环节区域渲染不出来)
- *
- * 源系统依据:
- * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowDefineListByEnterprise.do
- * -> 流程定义 HO_PERMIT_BREAK_ROAD(id 3768,名称「危险作业-断路作业」,编号前缀 DLZY)
- * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3768
- * -> 节点定义(共 10 个节点,1 级为作业申请)
- */
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { execFileSync } = require('child_process');
- const babel = require('@babel/core');
- const Module = require('module');
- 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-break-road-work');
- // 既有作业类型的实现文件:断路作业实现不得改动它们
- // (路径相对仓库根,便于 git diff 精确比对)
- const EXISTING_FILES = [
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/fireFlowNodes.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/fireFlowTask.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/confinedSpaceFlowNodes.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/confinedSpaceFlowTask.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/heightFlowNodes.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/heightFlowTask.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/hoistingFlowNodes.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/hoistingFlowTask.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/electricityFlowNodes.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/electricityFlowTask.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/breakGroundFlowNodes.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/breakGroundFlowTask.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/fireTicketDetail.vue',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/flowNodeForm.vue',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/confinedSpaceFlow.vue',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/heightFlow.vue',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/hoistingFlow.vue',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/electricityFlow.vue',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/components/breakGroundFlow.vue',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/templates.js',
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/workTypes.js'
- ];
- let failed = 0;
- function check(name, cond, extra) {
- if (cond) {
- console.log(' PASS ' + name);
- } else {
- failed++;
- console.log(
- ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
- );
- }
- }
- /**
- * 取文件的 git diff 文本
- * @param {string} file 相对仓库根的文件路径
- * @returns {string}
- */
- function gitDiff(file) {
- try {
- return execFileSync('git', ['diff', 'HEAD', '--', file], {
- cwd: ROOT,
- encoding: 'utf8',
- maxBuffer: 1024 * 1024 * 20
- });
- } catch (e) {
- return '';
- }
- }
- /**
- * 取 diff 里被删除的行(去掉 diff 头部的 --- / ---)
- * @param {string} diff
- * @returns {string[]}
- */
- function removedLines(diff) {
- return (diff || '')
- .split(/\r?\n/)
- .filter((line) => line.startsWith('-') && !line.startsWith('---'))
- .map((line) => line.slice(1).trim());
- }
- /**
- * 读取 detail.vue 源码
- * @returns {string}
- */
- function detailSrc() {
- return fs.readFileSync(path.join(SRC, 'detail.vue'), 'utf8');
- }
- // 编译 ESM -> CJS
- // breakRoadFlowTask.js 引用了 fireFlowTask.js(只读复用),
- // fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来
- fs.mkdirSync(OUT, { recursive: true });
- [
- 'breakRoadFlowNodes.js',
- 'breakRoadFlowTask.js',
- 'fireFlowNodes.js',
- 'fireFlowTask.js'
- ].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 br = require(path.join(OUT, 'breakRoadFlowNodes.js'));
- const bt = require(path.join(OUT, 'breakRoadFlowTask.js'));
- console.log(
- '\n[1] 动火 / 受限空间 / 高处作业 / 吊装作业 / 临时用电作业 / 动土作业既有实现未被改动'
- );
- EXISTING_FILES.forEach((file) => {
- let changed = true;
- try {
- execFileSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
- cwd: ROOT,
- stdio: 'ignore'
- });
- changed = false;
- } catch (e) {
- changed = true;
- }
- check(file.split('/').pop() + ' 与 HEAD 一致', !changed);
- });
- console.log('\n[2] detail.vue 只做新增(既有作业类型的分发逻辑一行未删)');
- const DETAIL =
- 'src/views/bpm/handleTask/components/dangerousWorkTicket/detail.vue';
- const detailDiff = gitDiff(DETAIL);
- const detailRemoved = removedLines(detailDiff);
- // 允许删除的行:仅文件头注释里的「六类作业」说明,
- // 以及组件注册里因补逗号而重排的 BreakGroundFlow
- const ALLOWED_REMOVED = [
- '动火、受限空间、高处作业、吊装作业、临时用电作业、动土作业六类作业共用本页面与 submit.vue,按作业类型分发到各自的组件:',
- 'BreakGroundFlow'
- ];
- const unexpectedRemoved = detailRemoved.filter(
- (line) => !ALLOWED_REMOVED.includes(line)
- );
- check(
- `detail.vue 的删除行仅为预期的 ${ALLOWED_REMOVED.length} 行`,
- unexpectedRemoved.length === 0,
- unexpectedRemoved
- );
- check(
- 'detail.vue 新增了断路作业的 tab 分发',
- /<break-road-flow[\s\S]{0,300}ref="breakRoadFlow"/.test(detailDiff)
- );
- check(
- 'detail.vue 新增了断路作业的当前环节计算属性',
- /currentBreakRoadNode\(\)\s*\{[\s\S]{0,200}matchBreakRoadFlowNode/.test(
- detailDiff
- )
- );
- check(
- 'detail.vue 新增了断路作业的取数 / 保存方法',
- /getBreakRoadTableValue\(\)/.test(detailDiff) &&
- /saveBreakRoad\(data\)/.test(detailDiff) &&
- /validateBreakRoadNodeForm\(\)/.test(detailDiff)
- );
- check(
- 'detail.vue 注册了断路作业组件',
- /import BreakRoadFlow from '\.\/components\/breakRoadFlow\.vue'/.test(
- detailSrc()
- ) && /BreakRoadFlow/.test(detailSrc())
- );
- check(
- 'detail.vue 的默认 tab 是「作业票信息」(不再自动切到「后续环节」)',
- /activeTab:\s*'info'/.test(detailSrc()) &&
- !/switchToFlowTab/.test(detailSrc())
- );
- console.log(
- '\n[3] 断路作业环节(源系统 HO_PERMIT_BREAK_ROAD 共 10 节点,1 级为作业申请模板)'
- );
- const 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', '完工验收']
- ];
- check('环节数量为 9', br.BREAK_ROAD_FLOW_NODES.length === 9, {
- len: br.BREAK_ROAD_FLOW_NODES.length
- });
- expected.forEach(([code, name], i) => {
- const node = br.BREAK_ROAD_FLOW_NODES[i];
- check(
- '第' + (i + 2) + '级 ' + code + ' / ' + name,
- node.code === code && node.name === name,
- { code: node.code, name: node.name }
- );
- });
- console.log('\n[4] 与源系统 formJson 一致性');
- const nodeOf = (k) => br.BREAK_ROAD_FLOW_NODES.find((n) => n.key === k);
- const colsOf = (k, fk) => {
- const node = nodeOf(k);
- const field = node.fields.find((f) => f.key === fk);
- return (field.columns || []).map((c) => c.label);
- };
- check(
- '风险辨识:结果为多选 20 项(源系统 checkbox)',
- nodeOf('risk').fields[0].type === 'checkbox' &&
- nodeOf('risk').fields[0].options.length === 20 &&
- nodeOf('risk').fields[0].options[0].label === '物体打击' &&
- nodeOf('risk').fields[0].options[19].label === '其它伤害'
- );
- check(
- '风险辨识:说明字段为多行文本(源系统 textarea)',
- nodeOf('risk').fields[1].type === 'textarea'
- );
- check(
- '风险辨识:说明字段别名不与多选框 key 冲突',
- !nodeOf('risk').fields[1].alias.includes('risk_result') &&
- !nodeOf('risk').fields[1].alias.includes('riskResult')
- );
- check(
- '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)',
- nodeOf('measure').fields[0].label === '安全措施确认表' &&
- JSON.stringify(colsOf('measure', 'measure_list')) ===
- JSON.stringify(['安全措施', '是否确认', '确认人'])
- );
- check(
- '现场安全措施确认:编制人签字 + 确认人签字',
- JSON.stringify(nodeOf('measure').fields.map((f) => f.label)) ===
- JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字'])
- );
- check(
- '现场安全措施确认:确认列为单选(不涉及 / 已确认)',
- nodeOf('measure')
- .fields[0].columns.find((c) => c.key === 'measure_confirm')
- .options.map((o) => o.label)
- .join(',') === '不涉及,已确认'
- );
- check(
- '现场安全措施确认:确认人列为单行文本(源系统 input)',
- nodeOf('measure').fields[0].columns.find((c) => c.key === 'measure_confirmer')
- .type === 'text'
- );
- check(
- '安全交底:两个签字(安全交底人签字 + 接受交底人签字)',
- JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) ===
- JSON.stringify(['安全交底人签字', '接受交底人签字']) &&
- nodeOf('brief').fields.every((f) => f.type === 'sign')
- );
- [
- ['leader_opinion', '作业负责人意见'],
- ['unit_opinion', '所在单位意见']
- ].forEach(([key, label]) => {
- const node = nodeOf(key);
- check(
- label + ':意见 + 签名',
- node.fields.length === 2 &&
- node.fields[0].type === 'textarea' &&
- node.fields[1].type === 'sign',
- { labels: node.fields.map((f) => f.label) }
- );
- });
- check(
- '消防、安全部门意见:两个意见框 + 两个签字(消防部门 / 安全部门)',
- JSON.stringify(nodeOf('fire_safety_opinion').fields.map((f) => f.label)) ===
- JSON.stringify([
- '消防部门意见',
- '安全部门意见',
- '消防部门签字',
- '安全部门签字'
- ]) &&
- nodeOf('fire_safety_opinion').fields[0].type === 'textarea' &&
- nodeOf('fire_safety_opinion').fields[1].type === 'textarea' &&
- nodeOf('fire_safety_opinion').fields[2].type === 'sign' &&
- nodeOf('fire_safety_opinion').fields[3].type === 'sign'
- );
- // 签字项标题要能分辨出是谁签的:
- // 同一环节里不允许出现两个同名的「手写签名」,且标题沿用源系统的原始写法
- check(
- '签字项标题与源系统一致(不再统一写「手写签名」)',
- JSON.stringify(
- br.BREAK_ROAD_FLOW_NODES.flatMap((n) =>
- n.fields.filter((f) => f.type === 'sign').map((f) => f.label)
- )
- ) ===
- JSON.stringify([
- '编制人签字',
- '确认人签字',
- '安全交底人签字',
- '接受交底人签字',
- '作业负责人签字',
- '所在单位签字',
- '消防部门签字',
- '安全部门签字',
- '审批部门签字',
- '签字'
- ])
- );
- check(
- '同一环节内不存在重名的签字项(不会出现两个「手写签名」)',
- br.BREAK_ROAD_FLOW_NODES.every((n) => {
- const labels = n.fields
- .filter((f) => f.type === 'sign')
- .map((f) => f.label);
- return new Set(labels).size === labels.length;
- })
- );
- check(
- '「消防、安全部门意见」的两个签字标题互不相同',
- nodeOf('fire_safety_opinion').fields[2].label !==
- nodeOf('fire_safety_opinion').fields[3].label
- );
- check(
- '意见类环节的取值 key 互不相同(源系统多处复制同一 model,不能互相覆盖)',
- new Set(
- br.BREAK_ROAD_FLOW_NODES.filter((n) => n.fields[0].type === 'textarea').map(
- (n) => n.fields[0].key
- )
- ).size === 5
- );
- check(
- '审批部门意见:意见 + 签名',
- nodeOf('approver_opinion').fields.length === 2 &&
- nodeOf('approver_opinion').fields[0].type === 'textarea' &&
- nodeOf('approver_opinion').fields[1].type === 'sign'
- );
- check(
- '作业实施:实施开始时间 + 实施结束时间 + 事中记录表',
- JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) ===
- JSON.stringify(['作业实施开始时间', '作业实施结束时间', '事中记录表'])
- );
- check(
- '事中记录表 3 列(时间/记录内容/记录人)',
- JSON.stringify(colsOf('execute', 'during_record_list')) ===
- JSON.stringify(['时间', '记录内容', '记录人'])
- );
- check(
- '完工验收:意见 + 签名',
- nodeOf('acceptance').fields.length === 2 &&
- nodeOf('acceptance').fields[0].label === '意见' &&
- nodeOf('acceptance').fields[1].type === 'sign'
- );
- check(
- '安全措施默认清单 3 条与源系统 formData 逐条一致',
- br.DEFAULT_BREAK_ROAD_MEASURES.length === 3 &&
- br.DEFAULT_BREAK_ROAD_MEASURES[0] ===
- '作业前,指定交通组织方案,并已通知相关部门或单位' &&
- br.DEFAULT_BREAK_ROAD_MEASURES[1] ===
- '作业前,在断路的路口和相关道路上设置交通警示标志,在作业区域附近设置路栏、道路作业警示灯、寻向标灯交通警示设施' &&
- br.DEFAULT_BREAK_ROAD_MEASURES[2] === '夜间作业设置警示灯'
- );
- check(
- '默认行挂在 measure_list 字段上(defaultRows)',
- Array.isArray(nodeOf('measure').fields[0].defaultRows) &&
- nodeOf('measure').fields[0].defaultRows.length === 3 &&
- nodeOf('measure').fields[0].defaultRows[0].measure_content ===
- br.DEFAULT_BREAK_ROAD_MEASURES[0]
- );
- console.log('\n[5] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
- const m = br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD_THREETH');
- check('按任务定义 key 精确命中', m && m.key === 'measure', {
- key: m && m.key
- });
- check(
- '按节点名称命中',
- (br.matchBreakRoadFlowNode('现场安全措施确认') || {}).key === 'measure'
- );
- check(
- '按别名命中',
- (br.matchBreakRoadFlowNode('危险辨识') || {}).key === 'risk'
- );
- check(
- '按平台自定义 key 反向包含命中',
- (br.matchBreakRoadFlowNode('break_road_risk') || {}).key === 'risk'
- );
- check(
- '第七级按 code 命中消防、安全部门意见',
- (br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD_SEVENTH') || {}).key ===
- 'fire_safety_opinion'
- );
- check(
- '流程定义 key / 一级作业申请 key 不误命中后续环节',
- br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD') === null &&
- br.matchBreakRoadFlowNode('HO_PERMIT_BREAK_ROAD_ONETH') === null
- );
- check('空标识返回 null', br.matchBreakRoadFlowNode('', null) === null);
- console.log('\n[6] 运行时辅助(回显 / payload / 流程变量)');
- const measureNode = nodeOf('measure');
- const source = {
- measure_list: [
- { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' }
- ],
- measure_preparer_sign: '/tmp/a.png'
- };
- const loaded = bt.loadNodeValues(measureNode, [source]);
- check(
- '回显:按自身 key 取到已保存内容',
- loaded.measure_list.length === 1 &&
- loaded.measure_preparer_sign === '/tmp/a.png'
- );
- check(
- '回显:未保存的字段为 undefined',
- loaded.measure_confirmer_sign === undefined
- );
- const payload = bt.buildNodePayload(measureNode, {
- measure_list: [{ measure_content: 'x' }],
- measure_preparer_sign: '/tmp/a.png'
- });
- check(
- 'payload:保留原始类型(数组不转字符串)',
- Array.isArray(payload.measure_list) && payload.measure_list.length === 1
- );
- check(
- 'payload:空值不写入',
- !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload)
- );
- const vars = bt.buildNodeVariables(measureNode, {
- measure_list: [{ measure_content: 'x' }],
- measure_preparer_sign: '/tmp/a.png'
- });
- check(
- '流程变量:子表转 JSON 字符串、其余转字符串',
- typeof vars.measure_list === 'string' &&
- vars.measure_preparer_sign === '/tmp/a.png'
- );
- check(
- '回显:多选框的值不会串到风险辨识说明',
- bt.loadNodeValues(nodeOf('risk'), [{ risk_result: '9' }]).risk_result_desc ===
- undefined
- );
- const opinionPayload = bt.buildNodePayload(nodeOf('leader_opinion'), {
- leader_opinion: '同意',
- leader_sign: '/tmp/leader.png'
- });
- check(
- '作业负责人意见:意见 + 签名写入 payload 与流程变量',
- opinionPayload.leader_opinion === '同意' &&
- opinionPayload.leader_sign === '/tmp/leader.png' &&
- bt.buildNodeVariables(nodeOf('leader_opinion'), {
- leader_opinion: '同意'
- }).leader_opinion === '同意'
- );
- check(
- '消防、安全部门意见:两处意见互不覆盖',
- bt.buildNodePayload(nodeOf('fire_safety_opinion'), {
- fire_dept_opinion: '消防意见',
- safety_dept_opinion: '安全意见'
- }).fire_dept_opinion === '消防意见' &&
- bt.buildNodePayload(nodeOf('fire_safety_opinion'), {
- fire_dept_opinion: '消防意见',
- safety_dept_opinion: '安全意见'
- }).safety_dept_opinion === '安全意见'
- );
- check(
- '所在单位意见 / 审批部门意见的 payload 互不覆盖',
- bt.buildNodePayload(nodeOf('unit_opinion'), { unit_opinion: '单位意见' })
- .unit_opinion === '单位意见' &&
- bt.buildNodePayload(nodeOf('approver_opinion'), {
- approver_opinion: '审批意见'
- }).approver_opinion === '审批意见'
- );
- console.log('\n[7] 组件模板与脚本一致性(模板调用的方法必须已在组件中定义)');
- const compiler = require('vue-template-compiler');
- const parser = require('@babel/parser');
- /**
- * 收集组件脚本里已定义的成员名(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 {Set<string>} 被调用的成员名集合
- */
- 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 || [] };
- }
- const sfcPath = path.join(SRC, 'components/breakRoadFlow.vue');
- const sfcSource = fs.readFileSync(sfcPath, 'utf8');
- const sfc = compiler.parseComponent(sfcSource);
- const templateCheck = collectTemplateCalls(sfc.template.content);
- const members = collectComponentMembers(sfc.script.content);
- check(
- 'breakRoadFlow.vue 模板编译无错误',
- templateCheck.errors.length === 0,
- templateCheck.errors
- );
- const missing = [...templateCheck.calls].filter((name) => !members.has(name));
- check(
- `模板调用的 ${templateCheck.calls.size} 个方法均已定义`,
- missing.length === 0,
- missing
- );
- check(
- '模板确实调用了 getConfirmColumn / confirmAll(子表「全部确认」按钮)',
- templateCheck.calls.has('getConfirmColumn') &&
- templateCheck.calls.has('confirmAll')
- );
- check(
- '未识别到环节时展示当前任务名 / key(便于排查流程模型配置)',
- /taskLabel/.test(sfcSource)
- );
- check(
- '子表日期列渲染为日期时间选择器(el-date-picker)',
- /<el-date-picker[^>]*column\.type === 'date'/.test(sfcSource)
- );
- check(
- '作业实施:事中记录表「时间」列类型为 date',
- nodeOf('execute')
- .fields.find((f) => f.key === 'during_record_list')
- .columns.some((c) => c.key === 'record_time' && c.type === 'date')
- );
- check(
- '组件对外提供 validate / getPayload / getVariables',
- /validate\(\)\s*\{/.test(sfc.script.content) &&
- /getPayload\(\)\s*\{/.test(sfc.script.content) &&
- /getVariables\(\)\s*\{/.test(sfc.script.content)
- );
- check(
- '组件引用的节点 schema 与运行时辅助均来自断路作业自己的文件',
- /from '\.\.\/breakRoadFlowNodes'/.test(sfc.script.content) &&
- /from '\.\.\/breakRoadFlowTask'/.test(sfc.script.content)
- );
- // 其余作业类型组件只做只读体检,不参与断言(既有实现不做任何改动)
- [
- 'confinedSpaceFlow.vue',
- 'heightFlow.vue',
- 'hoistingFlow.vue',
- 'electricityFlow.vue',
- 'breakGroundFlow.vue'
- ].forEach((name) => {
- const otherSfc = compiler.parseComponent(
- fs.readFileSync(path.join(SRC, 'components', name), 'utf8')
- );
- const otherCheck = collectTemplateCalls(otherSfc.template.content);
- const otherMembers = collectComponentMembers(otherSfc.script.content);
- const otherMissing = [...otherCheck.calls].filter(
- (n) => !otherMembers.has(n)
- );
- console.log(
- ' INFO ' +
- name +
- ' 模板调用体检:缺失 ' +
- otherMissing.length +
- ' 个' +
- (otherMissing.length ? ' -> ' + JSON.stringify(otherMissing) : '')
- );
- });
- console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED'));
- process.exit(failed ? 1 : 0);
|