/** * 盲板抽堵作业票审批流校验 * * 用法:node scripts/check-blind-plate-work-flow-nodes.js * * 校验内容: * 1) 动火、受限空间、高处作业、吊装作业、临时用电作业、动土作业、断路作业的既有实现文件未被改动 * (与 HEAD 完全一致) * 2) detail.vue 只做「新增」:既有作业类型的分发逻辑一行未删 * 3) 盲板抽堵作业 7 个后续环节的 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_BLIND_PLATE(id 3763,名称「危险作业-盲板抽堵作业」,编号前缀 MBCDZY) * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3763 * -> 节点定义(共 8 个节点,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-blind-plate-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/breakRoadFlowNodes.js', 'src/views/bpm/handleTask/components/dangerousWorkTicket/breakRoadFlowTask.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/components/breakRoadFlow.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 // blindPlateFlowTask.js 引用了 fireFlowTask.js(只读复用), // fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来 fs.mkdirSync(OUT, { recursive: true }); [ 'blindPlateFlowNodes.js', 'blindPlateFlowTask.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 bp = require(path.join(OUT, 'blindPlateFlowNodes.js')); const bt = require(path.join(OUT, 'blindPlateFlowTask.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); // 允许删除的行:仅文件头注释里的「七类作业」说明, // 以及组件注册里因补逗号而重排的 BreakRoadFlow const ALLOWED_REMOVED = [ '动火、受限空间、高处作业、吊装作业、临时用电作业、动土作业、断路作业七类作业共用本页面与 submit.vue,按作业类型分发到各自的组件:', 'BreakRoadFlow' ]; const unexpectedRemoved = detailRemoved.filter( (line) => !ALLOWED_REMOVED.includes(line) ); check( `detail.vue 的删除行仅为预期的 ${ALLOWED_REMOVED.length} 行`, unexpectedRemoved.length === 0, unexpectedRemoved ); check( 'detail.vue 新增了盲板抽堵作业的 tab 分发', / { const node = bp.BLIND_PLATE_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) => bp.BLIND_PLATE_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( '风险辨识:说明字段为单行文本(源系统 input)', nodeOf('risk').fields[1].type === 'text' ); 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( '现场安全措施确认:安全措施列为多行文本(源系统 textarea)', nodeOf('measure').fields[0].columns.find((c) => c.key === 'measure_content') .type === 'textarea' ); 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, signLabel]) => { const node = nodeOf(key); check( label + ':意见 + 签名', node.fields.length === 2 && node.fields[0].type === 'textarea' && node.fields[0].label === label && node.fields[1].type === 'sign' && node.fields[1].label === signLabel, { labels: node.fields.map((f) => f.label) } ); }); check( '签字项标题能分辨出是谁签的(同一环节内不重名)', bp.BLIND_PLATE_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( '意见类环节的取值 key 互不相同(源系统多处复制同一 model,不能互相覆盖)', new Set( bp.BLIND_PLATE_FLOW_NODES.filter( (n) => n.fields[0].type === 'textarea' ).map((n) => n.fields[0].key) ).size === 3 ); check( '作业实施:实施开始时间 + 事中记录表(源系统只有开始时间,没有结束时间)', JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) === JSON.stringify(['作业实施开始时间', '事中记录表']) ); check( '事中记录表 3 列(时间/记录内容/记录人)', JSON.stringify(colsOf('execute', 'during_record_list')) === JSON.stringify(['时间', '记录内容', '记录人']) ); check( '事中记录表「时间」列为日期时间(源系统 datePicker)', nodeOf('execute') .fields.find((f) => f.key === 'during_record_list') .columns.some((c) => c.key === 'record_time' && c.type === 'date') ); check( '完工验收:意见 + 签名', nodeOf('acceptance').fields.length === 2 && nodeOf('acceptance').fields[0].label === '意见' && nodeOf('acceptance').fields[1].type === 'sign' ); check( '安全措施默认清单 10 条与源系统 formData 逐条一致', bp.DEFAULT_BLIND_PLATE_MEASURES.length === 10 && bp.DEFAULT_BLIND_PLATE_MEASURES[0] === '在管道、设备上作业时,降低系统压力,作业点应为常压或微正压' && bp.DEFAULT_BLIND_PLATE_MEASURES[1] === '在有毒介质的管道、设备上作业时,尽可能降低系统压力,作业点应为常压' && bp.DEFAULT_BLIND_PLATE_MEASURES[2] === '在有毒介质的管道、设备上作业时,作业人员穿戴适合的防护用具' && bp.DEFAULT_BLIND_PLATE_MEASURES[3] === '易燃易爆场所,作业人员穿防静电工作服、工作鞋;作业时使用防爆灯具和防爆工具' && bp.DEFAULT_BLIND_PLATE_MEASURES[4] === '易燃易爆场所,距作业地点30m内无其他动火作业' && bp.DEFAULT_BLIND_PLATE_MEASURES[5] === '在强腐蚀性介质的管道、设备上作业时,作业人员已采取防止酸碱灼伤的措施' && bp.DEFAULT_BLIND_PLATE_MEASURES[6] === '介质温度较高、可能造成烫伤的情况下,作业人员已采取防烫措施' && bp.DEFAULT_BLIND_PLATE_MEASURES[7] === '同一管道上不同时进行两处以上的盲板抽堵作业' && bp.DEFAULT_BLIND_PLATE_MEASURES[8] === '其它相关特殊作业已办理相应安全作业票' && bp.DEFAULT_BLIND_PLATE_MEASURES[9] === '作业现场四周已设警戒区' ); check( '默认行挂在 measure_list 字段上(defaultRows)', Array.isArray(nodeOf('measure').fields[0].defaultRows) && nodeOf('measure').fields[0].defaultRows.length === 10 && nodeOf('measure').fields[0].defaultRows[0].measure_content === bp.DEFAULT_BLIND_PLATE_MEASURES[0] ); console.log('\n[5] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)'); const m = bp.matchBlindPlateFlowNode('HO_PERMIT_BLIND_PLATE_THREETH'); check('按任务定义 key 精确命中', m && m.key === 'measure', { key: m && m.key }); check( '按节点名称命中', (bp.matchBlindPlateFlowNode('现场安全措施确认') || {}).key === 'measure' ); check( '按别名命中', (bp.matchBlindPlateFlowNode('危险辨识') || {}).key === 'risk' ); check( '按平台自定义 key 反向包含命中', (bp.matchBlindPlateFlowNode('blind_plate_risk') || {}).key === 'risk' ); check( '第五级按 code 命中作业负责人意见', (bp.matchBlindPlateFlowNode('HO_PERMIT_BLIND_PLATE_FIVETH') || {}).key === 'leader_opinion' ); check( '流程定义 key / 一级作业申请 key 不误命中后续环节', bp.matchBlindPlateFlowNode('HO_PERMIT_BLIND_PLATE') === null && bp.matchBlindPlateFlowNode('HO_PERMIT_BLIND_PLATE_ONETH') === null ); check('空标识返回 null', bp.matchBlindPlateFlowNode('', 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( '所在单位意见 / 完工验收的 payload 互不覆盖', bt.buildNodePayload(nodeOf('unit_opinion'), { unit_opinion: '单位意见' }) .unit_opinion === '单位意见' && bt.buildNodePayload(nodeOf('acceptance'), { acceptance_opinion: '验收意见' }).acceptance_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} 成员名集合 */ 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} 被调用的成员名集合 */ 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/blindPlateFlow.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( 'blindPlateFlow.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)', /]*column\.type === 'date'/.test(sfcSource) ); 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 '\.\.\/blindPlateFlowNodes'/.test(sfc.script.content) && /from '\.\.\/blindPlateFlowTask'/.test(sfc.script.content) ); // 其余作业类型组件只做只读体检,不参与断言(既有实现不做任何改动) [ 'confinedSpaceFlow.vue', 'heightFlow.vue', 'hoistingFlow.vue', 'electricityFlow.vue', 'breakGroundFlow.vue', 'breakRoadFlow.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[8] 作业票内容(基本信息)的手写签名能正常显示'); // 作业票内容里存的是签名文件存储路径(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 templatesSrc = fs.readFileSync(path.join(SRC, 'templates.js'), 'utf8'); 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' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED')); process.exit(failed ? 1 : 0);