车间的设备保养一直靠纸质工单:保养计划下来,一张单子写清设备编号、保养项目、执行人,保养完签字归档。现在要把这张单子搬进设备管理系统。但仔细一想,麻烦的地方不止「搬」——这张单子在系统里得能联动:填了设备编号,设备名称、型号、安装位置要自动带出来;保养完的数据要进工单台账,还要能触发下一次保养计划。
所以真正要设计的不是一张表单,而是这张表单和系统之间的数据怎么流动。这篇文章不复盘「怎么把表画出来」,只复盘「怎么把数据接对」。
一、先别急着搭页面,把数据流向倒着理一遍
动手前先看终点:这张保养工单上线后,数据最终要去哪?列出来是三个端点——表单本身(采集)、设备系统的工单台账(落库)、保养计划模块(触发下次保养)。数据不是「填完存起来」就结束了,它要在这三个端点之间来回流动。
把流向理清楚,才能定字段。比如「设备编号」不是孤零零一个文本,它是联动的锚点:它的值决定了设备名称、型号、位置从哪来,也决定了保养项目该带出哪几条。字段路径如果在这一步没定死,后面回填、落库、触发计划全都会对不上。
二、自研把表单写进系统,会卡在三个地方
传统做法是把保养工单当作系统的一个普通页面来写。推演一遍,很快会撞上三个绕不开的问题:
样式还原不了。 保养工单是排版规整的正式单据,合并单元格、签字栏、盖章位置都有固定版式。用通用表单组件一个个拼,拼出来总是走样,老师傅一眼就能瞧出不对版。
字段联动靠手写,且每张表各写一遍。 选了设备编号带出设备信息,写一次;物料编码联动,再写一次;点检表、维修单、保养单,每种表单各写一套。代码越堆越厚,改一处漏一处。
改一处发一次版。 保养项目调整是常态,今天加一项、明天改个判定标准,每次都要走开发、测试、上线的完整流程。
字段联动的问题用一段伪代码看最清楚:
// 自研写法:联动逻辑散落在每张表单里,重复且难维护
function onDeviceChange() {
const code = deviceInput.value;
fetch(`/api/device/${
code}`).then(r => r.json()).then(d => {
deviceName.value = d.name; // 手动逐个赋值
deviceModel.value = d.model;
deviceLocation.value = d.location;
// 换一张表单(点检表、维修单),上面三行还要再写一遍
});
}
问题不在于这三行难写,而在于它会被复制到系统的每一个表单里。
三、翻车实录:两个进了真实环境才暴露的坑
把表单作为独立组件嵌进系统后,有两个坑是本地开发时发现不了、一上真实环境就冒出来的。
坑一:iframe 高度塌陷。 表单嵌进页面,高度留了固定值,保养项目一多表单变高,底部就被裁掉一截;表单短的时候,又留出一大片空白。根因是 iframe 的高度不会随内容自动伸缩。
解法是让表单内部把内容高度主动上报,宿主侧监听后调整:
// 表单组件内部:内容高度变化时通过 postMessage 上报
const observer = new ResizeObserver(([entry]) => {
parent.postMessage(
{
command: 'RESIZE', data: {
height: entry.contentRect.height } },
parentOrigin
);
});
observer.observe(document.body);
// 宿主系统侧:监听到上报,调整 iframe 高度
window.addEventListener('message', (e) => {
if (e.origin !== formUrl) return;
if (e.data.command === 'RESIZE') {
iframe.style.height = e.data.data.height + 'px';
}
});
坑二:origin 校验。 图省事把 targetOrigin 写成 '*',或者干脆不校验消息来源,等于把表单的数据读写接口敞开着,任何页面都能往里写数据。跨域通信必须双向校验 origin——收的时候验来源,发的时候指明目标:
const FORM_ORIGIN = new URL(formUrl).origin; // 取协议+主机+端口
// 收:只接受表单域发来的消息,其余直接丢弃
window.addEventListener('message', (e) => {
if (e.origin !== FORM_ORIGIN) return;
// ... 处理
});
// 发:指明目标域,不用 '*'
iframe.contentWindow.postMessage({
command, data }, FORM_ORIGIN);
这两个坑有个共同点:单看每一行都不难,但集成时漏掉任何一处,都会在投产后才以「页面显示不对」「数据被串改」的形式爆出来。
四、字段联动回填:选一个编号,带出一串信息
保养工单最高频的联动,就是选设备编号带出设备信息。实现的要点是「取一次、按路径分发」:先从系统接口把设备数据一次性取回,再按字段路径写进表单,而不是每填一格发一次请求。
// 选设备编号 → 一次性取回设备信息 → 按字段路径回填
async function onDeviceSelected(deviceCode) {
const raw = await fetch(`/api/device/${
deviceCode}`).then(r => r.json());
// 字段路径:. 访问对象属性,# 访问数组元素
await rpc.SET_DATA({
'device.code': raw.code,
'device.name': raw.name,
'device.model': raw.model,
'device.location': raw.location,
// 保养项目是数组,一行一条,随设备类型带出不同条数
'items': raw.items.map(i => ({
'content': i.content, 'standard': i.standard }))
});
}
这里能看出字段路径设计的价值:device.name、device.model 是对象字段,items 是数组字段(对应动态行)。保养项目这种「同一种表、不同设备项目条数不同」的场景,正好靠数组字段驱动行数,带出几条就渲染几行。
五、数据旅程:一张工单的三段流转
回到最初理的那条数据流。一张保养工单在系统里实际走三段路:
- 填的时候:选设备编号,设备信息从系统接口按字段路径回填进表单;
- 提交的时候:表单数据回写设备系统的工单台账,按字段路径落到对应的表字段;
- 落库之后:台账数据触发保养计划模块,更新下一次保养的时间。
整条链路上,表单只负责「长什么样、怎么采」,系统负责「存哪里、流向哪」,中间靠字段路径和通信协议对齐。这也解释了为什么字段路径要在动工前就定死——它是这条链路上唯一贯通三段的「通用语」。
六、技术选型
上面这一整套——模板还原、iframe 嵌入、postMessage 通信、字段路径回填——都是通用技术,任何团队都能照着搭。真正要选的是表单组件本身:样式能不能原样还原、字段路径规则是否稳定、嵌入和通信协议是否已经封装好、高度自适应和 origin 校验这类细节是否处理过。
表单组件这一层,最终选型落在 FlashTable 这款表单开发工具上。研发团队看中的几个特征:
- 把 Word/Excel 模板 1:1 还原成线上表单,保养工单这类正式单据的合并单元格、签字栏、边框都能保留;
- 以 iframe 组件形态嵌入,内置 postMessage 通信协议,封装了 LOAD_DATA/SET_DATA/GET_DATA/SUBMIT_DATA 等命令和生命周期事件,高度自适应、来源校验这类集成细节已经处理;
- 字段按
./#路径命名,支持动态行,设备信息按路径回填、数据能结构化进入工单台账。
七、复盘
回头看,一张保养工单上线,核心工作量不在「把表画出来」,而在「把数据接对」:字段路径怎么定、联动回填怎么触发、通信怎么校验、高度怎么自适应。这几件事理顺了,表单就能稳定地嵌在系统里跑;理顺不了,再精致的表单也只是个孤岛。
设备保养、点检、维修这类和系统强关联的表单,交给 FlashTable 这类表单开发工具去承接——模板原样还原、按字段路径联动回填、以组件形态嵌入系统、数据结构化落库——比把每张表都写进系统代码里要务实得多。