HarmonyOS 多设备表单连载01:字段主键与草稿状态建模【共创稿事节】
现场巡检表单往往要在不同尺寸的窗口中使用。录入人员可能先选择现场位置,随后补充故障描述,再调整优先级。如果页面直接把三个输入框的显示文案当作数据字段,后续更换语言、调整文案或增加宽屏布局时,同一项数据就容易被写入不同位置。表单的第一步不是设计按钮颜色,而是建立可以长期稳定识别的字段主键。
FormContinuity以“现场设备巡检”为业务主线。首个版本只收集现场位置、问题摘要、优先级三项信息,使用site、issue、priority作为稳定字段键。用户看到的“现场地点”“问题摘要”“优先级”属于界面文案,随页面与语言资源变化;内部字段键参与变更记录、草稿保存与下一章节的数据恢复,生命周期更长。当前草稿标识为site-check,数据结构版本为schema=1。

一、先把字段身份与展示属性分开
一个输入项至少包含字段键、显示标签、输入类型、是否必填和当前值。字段键应该在同一表单定义中唯一,标签则可以随设计调整。把主键固定在模型层,是为了防止“问题摘要”改成“异常描述”后被系统误认为增加了第四个字段。与此对应,保存草稿时只保存键值字典和必要的版本信息,不把显示标签复制到每条业务记录中。
为了控制复杂度,三项字段分别采用单行文本、多行文本与枚举选择。现场位置需要去除首尾空白;问题摘要可以保留中间的换行,但要约束长度;优先级只允许“低”“中”“高”三个值。界面收到原始输入后先调用字段规则,再将规范化后的值提交给草稿对象。这样,手机上的单列输入与平板上的双列排布都使用同一个字段定义,显示层不承担业务校验职责。
以下代码用Node.js中的普通JavaScript描述字段定义与初始快照。字段键排列顺序用于渲染,不依赖对象遍历的偶然顺序;创建草稿时必须为每个已知字段提供值,后续遇到未知字段由明确规则处理,而不是直接合并进正式数据。
const FIELD_DEFS = Object.freeze([
{ key: 'site', label: '现场地点', kind: 'text', required: true },
{ key: 'issue', label: '问题摘要', kind: 'textarea', required: true },
{ key: 'priority', label: '优先级', kind: 'select', required: true }
]);
function createDraft() {
return {
draftId: 'site-check', schema: 1, revision: 0,
savedRevision: 0, status: 'CLEAN',
fields: {
site: '北区巡检点',
issue: '设备标签磨损',
priority: '中'
},
changedKeys: []
};
}
console.log(FIELD_DEFS.length, createDraft().revision); // 3 0
二、修订号表达一次有效业务变更
文本输入过程中可能触发多次回调,但每次回调不一定都对应真实修改。比如输入框重新获得焦点,或者用户先输入空格又被规范化规则移除,最终值可能与原值相同。此时增加修订号会制造虚假的待保存状态,还会让下一章节误以为草稿已经发生更新。因此修订号的增长条件应是已提交字段值确实发生变化。
一个草稿使用全局revision记录本地修改顺序,savedRevision记录最近一次确认写入存储的版本。两者不同表示存在尚待保存的修改。changedKeys记录发生变化的字段,避免界面刷新时重新比较全部字段。这份记录不是远端同步协议,也不承担跨设备时钟排序;它首先解决同一编辑会话内“当前数据和已保存数据是否一致”的问题。
字段更新函数使用白名单主键、输入规范化与值相等比较三道检查。未知字段应抛出业务错误,防止拼写错误悄悄产生新属性。更新成功后返回新对象,避免多个组件持有同一个可变引用;若值未变化,直接返回旧对象,保持修订号稳定。
function normalizeField(key, value) {
if (typeof value !== 'string') throw new TypeError('FIELD_TYPE');
if (key === 'site') return value.trim();
if (key === 'issue') return value.trim().replace(/\r\n/g, '\n');
if (key === 'priority') return value.trim();
throw new Error('UNKNOWN_FIELD');
}
function editDraft(draft, key, input) {
if (!['site', 'issue', 'priority'].includes(key))
throw new Error('UNKNOWN_FIELD');
const nextValue = normalizeField(key, input);
if (draft.fields[key] === nextValue) return draft;
return {
...draft, revision: draft.revision + 1, status: 'DIRTY',
fields: { ...draft.fields, [key]: nextValue },
changedKeys: [...new Set([...draft.changedKeys, key])]
};
}
const afterEdit = editDraft(createDraft(), 'issue', '设备标签磨损,需补贴');
console.log(afterEdit.revision, afterEdit.status, afterEdit.changedKeys);
三、界面状态应由模型派生而不是手工拼接
编辑页面需要显示输入值、提示信息与草稿状态,但无需再建立一套独立的“是否已修改”布尔数组。可将revision > savedRevision作为待保存条件,再把错误字典与当前字段映射到输入组件。这样,在页面滚动、窗口切换或局部组件重建之后,界面仍能根据同一快照恢复显示,不受组件创建顺序影响。
ArkUI的TextInput支持onChange回调,开发者可将输入内容交给状态处理函数;多行输入可以使用TextArea。华为关于文本输入和自定义键盘的文档也描述了输入值更新、焦点处理与光标位置调整。界面更新本地状态时,应保留输入法编辑过程,不在每一次字符变化中执行耗时存储操作;业务校验可以在输入后集中展示,避免把高频输入事件与磁盘写入绑在一起。
本阶段对错误采用字段粒度的结果结构:地点为空标记SITE_REQUIRED;摘要过短标记ISSUE_TOO_SHORT;优先级不在选项集合中标记PRIORITY_INVALID。校验结果可以用于按钮可用性和提示文字,但不能覆盖原始输入值,否则用户可能无法修正错误。错误只反映当前字段内容,不直接决定页面是否销毁草稿。
function validateDraft(draft) {
const errors = {};
if (!draft.fields.site.trim()) errors.site = 'SITE_REQUIRED';
if (draft.fields.issue.trim().length < 5)
errors.issue = 'ISSUE_TOO_SHORT';
if (!['低', '中', '高'].includes(draft.fields.priority))
errors.priority = 'PRIORITY_INVALID';
return { valid: Object.keys(errors).length === 0, errors };
}
function viewState(draft) {
const validation = validateDraft(draft);
return {
revision: draft.revision,
dirty: draft.revision !== draft.savedRevision,
fields: { ...draft.fields },
validation
};
}
const draft = editDraft(createDraft(), 'issue', '设备标签磨损,需补贴');
console.log(viewState(draft));

四、表单目录只承担可解释的职责
在工程中,model/FieldSchema.ets描述稳定字段定义,model/DraftSnapshot.ets描述草稿快照,logic/DraftReducer.ets接收编辑动作,pages/InspectionPage.ets负责页面布局。业务对象不保存组件实例或输入框控制器,因此相同的字段校验可以被不同尺寸的页面复用。下一章新增storage/DraftRepository.ets时,只需接受快照,不必改变字段更新逻辑。
页面初次进入时,用默认字段值生成revision=0的干净快照。用户把摘要改成“设备标签磨损,需补贴”后,修订号增加到1,状态转为DIRTY,changedKeys为['issue']。现场地点仍为“北区巡检点”,优先级仍为“中”。三个字段的身份、顺序和数据格式保持不变,避免后续新增页面时出现两个版本的表单定义。
组件层可以显示“待保存”提示,但应与实际写入结果区分。只有存储层确认对应修订号已经写入,才能把savedRevision推进到同一数值并显示SAVED。在本章,保存尚未接入,界面将该修改标记为待处理;下一章会建立序列化格式、存储键和恢复流程。提前区分两种状态,可以防止用户以为点击输入框就已经完成草稿保存。
五、调试时检查不变量而不是只看页面文案
可以围绕三个简单的不变量组织检查:字段集合始终等于定义的三个主键;重复提交相同内容不会增加revision;当revision与savedRevision不同,状态必须为DIRTY。对于无效优先级、空地点、摘要太短等输入,校验结果应具有稳定错误码,便于页面按语言资源映射提示,而不是将任意异常字符串直接展示给用户。
对文本输入还要关注光标与选区。若在onChange中立即重新格式化并重建整个输入控件,光标可能跳至末尾;更稳妥的做法是区分编辑中的临时文本与已提交业务值,在适当的提交节点规范化。华为的文本输入问题说明讨论了onTextSelectionChange与光标控制,这可作为处理格式化文本的参考。表单字段主键的稳定性与输入体验并不冲突,关键是避免在一次键盘事件中执行过多副作用。
当前页面业务状态应能明确读出draftId=site-check、schema=1、revision=1、savedRevision=0、DIRTY,以及修改字段issue。其他设备尺寸虽然可以调整卡片间距与列数,但仍必须展示相同的业务数据。后续讨论本地草稿恢复时,将以这份确定的快照作为唯一输入,而不重新造一套字段模型。

六、为后续保存与冲突合并留出接口
为了便于后续扩展,草稿对象将schema与revision分开:schema用于判断数据结构是否需要迁移,revision表示当前表单已经发生多少次有效编辑。即使两次修订号相同,只要结构版本不同也不能直接混用。draftId则限定同一业务表单的存储位置,防止另一个巡检表单覆盖当前内容。
这一章先把稳定字段、更新规则和错误字典收敛到应用层。下一章将补齐保存确认和重新进入页面的恢复顺序;后续章节才会引入字段级冲突比较与不同窗口的布局安排。把复杂功能分阶段建立在同一个模型之上,能够降低每一阶段的排查范围,也让实际业务数据的来源和变化过程保持清晰。
更多推荐



所有评论(0)