开始之前:本篇在系列中的位置

本文是“HarmonyOS ArkTS 工业 App 实战”系列的第 001 篇,不要求具备注塑行业经验,也不要求提前掌握复杂的状态管理。开始前只需准备一个可以正常打开的 HarmonyOS 工程,并能够找到 ArkTS 页面文件。

本篇将完成注塑机调试记录的基础业务闭环:用户填写工艺参数,页面校验并转换输入数据,随后创建调试记录,通过 @State 更新记录数组,最终驱动列表重新渲染。完成后,你应该能够独立追踪“输入 → 校验 → 对象创建 → 状态更新 → 列表刷新”的完整过程,并定位保存后列表没有变化的常见原因。

当前记录仅保存在页面内存中,应用退出后不会保留数据。后续文章可以在这个数据模型上继续增加本地持久化,以及机台、产品和模具档案关联;正式选题尚未确认前,本文不提供虚构的标题或链接。

前言

本文从"注塑工程师助手"App 的核心功能模块切入——注塑机调试记录。

在注塑现场,工程师需要记录机台、产品、模具、材料、温度、压力和周期。若数据散落在纸张或 Excel 中,后续复盘会很困难。

同一产品再次投产时,工程师需要快速找回设备、温度、压力与缺陷记录。缺少结构化历史数据,换班或换料后就可能重复调试。

移动 App 可以把调机过程变成结构化字段,并为查询与统计提供数据基础。这个场景也能串联 ArkTS 数据模型、表单、状态和列表渲染。

本文将基于 HarmonyOS ArkTS API 24,实现一个完整的调机记录页面,重点覆盖以下三方面内容:

  1. 使用 ArkTS 定义调机记录数据模型
  2. 基于 ArkUI 构建工艺参数录入表单
  3. 通过 @State 装饰器驱动记录列表自动刷新

本文不追求一次性完成整个 App,而是先跑通一个真实业务模块。后续文章将陆续扩展机台档案、产品档案、生产记录、异常记录及报表统计等功能。

阅读时建议关注两条主线:业务线——一条调机记录包含哪些核心信息;代码线——用户输入如何从表单流入状态变量,再从状态变量驱动列表渲染。打通这两条链路后,后续构建更复杂的生产记录模块将事半功倍。

效果预览

下面用演示截图确认页面结构,所有机台与工艺参数均为示例值。

截图 1:页面总览。先观察统计区与记录列表的上下关系。
在这里插入图片描述

截图 2:调机参数表单。工程师可录入机台、模具、产品、材料、温度、压力及成型周期等完整参数。
在这里插入图片描述

截图 3:保存后的记录列表。新增记录自动置顶,按时间倒序排列便于查阅。
在这里插入图片描述

截图 4:错误反馈。输入不能转换为正数时,保存流程会停在校验阶段。
在这里插入图片描述

开发环境

先确认本文使用的版本基线:

HarmonyOS:6.1.1
API Version:24
开发语言:ArkTS
开发工具:DevEco Studio 6.1.1

功能目标

本页面需实现的功能如下表所示:

功能 说明
基础信息录入 机台编号、产品名称、模具编号、材料
工艺参数录入 料筒温度、模具温度、注射压力、保压压力、成型周期
调机记录保存 点击按钮生成一条新的调试记录
记录列表展示 最新记录自动显示在列表顶部
状态驱动刷新 records 数据变更后,页面自动重新渲染

一、项目结构与文件规划

为了让教程更贴近真实项目,我们先明确代码组织结构。

本篇示例建议独立为一个页面文件:

entry/src/main/ets/pages/InjectionDebugPage.ets

若后续继续扩展"注塑工程师助手"App,可按以下模块拆分页面:

系列按页面职责拆分,文件名对应业务边界:

  • InjectionDebugPage.ets:调机记录闭环。
  • InjectionMachinePage.ets:机台档案。
  • ProductArchivePage.ets:产品档案。
  • ProductionRecordPage.ets:生产批次。
  • ExceptionRecordPage.ets:异常处理。
    本篇只修改第一个页面,其余留待扩展。

模块化拆分有两点好处:

  1. 页面与文件一一对应,读者一目了然
  2. 系列文章每篇都有明确的代码落点,内容体系更清晰

二、数据模型设计

首先定义一条调机记录所需的字段结构:

完整模型代码统一放在“十四、完整代码”,这里先理解字段职责。

以上代码定义了一条完整的调试记录。首版字段设计保持精简,重点关注页面逻辑与状态流转。

可以按现场使用顺序理解字段:

  • 先用 machineNo 确认记录属于哪台设备。
  • 再用 moldNo 定位本次安装的模具。
  • productName 说明这次试制的产品。
  • barrelTemp 保存料筒温度,单位为摄氏度。
  • injectionPressure 保存注射压力,单位为 MPa。
  • holdingPressure 保存保压压力,单位同为 MPa。
  • moldTemp 保存模具温度,单位为摄氏度。
  • cycleTime 保存单次成型周期,单位为秒。
  • result 记录正常、缩水或飞边等现场结论。

后续若构建完整生产系统,可继续扩展班次、批次、操作员、图片附件、异常分类等字段。

三、页面整体布局架构

页面分为三大区域,布局结构如下:

Scroll
└── Column
    ├── 顶部标题区
    ├── 调机参数表单区
    └── 最近调试记录列表

外层使用 Scroll,内部嵌套 Column,让多字段表单可以上下滚动。先搭骨架,再填充组件细节。

四、逐段代码解析

下面按代码顺序逐步拆解,帮助你不仅知道"代码能跑",更理解每一段在页面中承担的职责。

1. InjectionDebugRecord:定义业务数据模型

字段声明可在篇末完整代码中逐项对照。

这段代码是整个页面的数据基石。页面上每一张"调试记录卡片",背后都对应一个 InjectionDebugRecord 实例。

字段设计遵循一个原则:优先保留工程师复盘时最核心的信息machineNo 用于定位机台,productNamemoldNo 用于定位产品与模具,material 记录原材料,温度、压力、周期则是调机过程的关键工艺参数。

数据模型不必一次覆盖所有需求。第一版先完成核心闭环,附件、班次、操作员和异常分类可在需求明确后扩展。

2. constructor:确保记录对象可完整实例化

构造方法的完整参数与赋值顺序保留在篇末,阅读时重点核对一一对应关系。

构造方法负责将传入参数赋值给对象属性。

用户点击"保存调机记录"时,程序从表单中取出机台编号、产品名称、温度、压力等数据,通过 new InjectionDebugRecord(...) 创建一条新记录。构造方法相当于一个装配入口,确保每条记录都按照统一的字段顺序生成。

注意一个细节:id 放在参数列表第一位。后续列表渲染时,ForEach 需要稳定的唯一标识来区分每一项,id 正承担此角色。缺少唯一 key 可能导致列表刷新时项与项的对应关系错乱。

3. @Entry 与 @Component:声明页面组件

@Entry
@Component
struct InjectionDebugPage {
}

@Component 表示这是一个 ArkUI 组件,内部可定义状态变量、业务方法及 build() 渲染函数。

@Entry 表示该组件可作为入口页面独立运行。两个装饰器共同向框架声明:这个 struct 并非普通结构体,而是一个可渲染到屏幕上的完整页面组件。

页面命名为 InjectionDebugPage,语义清晰。真实项目中,建议页面命名尽量与业务模块对应,便于后期维护。

4. @State:表单输入与页面状态的绑定

@State machineNo: string = 'IM-120T-01'
@State productName: string = '外壳件 A01'
@State moldNo: string = 'MOLD-A01'
@State material: string = 'ABS'
@State barrelTemp: string = '230'
@State moldTemp: string = '65'
@State injectionPressure: string = '85'
@State holdingPressure: string = '58'
@State cycleTime: string = '32'
@State result: string = '调试正常'
@State errorMessage: string = ''

这些 @State 变量用于存储表单的当前输入值。

为什么温度、压力等数值型参数先用 string 保存? 因为它们来自 TextInput 组件,输入框天然返回文本类型。输入阶段用字符串存储更便捷,待点击保存时再统一转为数字,表单处理逻辑更简洁。

barrelTemp 为例:页面初始化时默认值为 230,用户在输入框中修改为 235 后,barrelTemp 的值会同步更新。点击保存时,再将其转换为 number 类型写入调试记录。

errorMessage 是校验提示状态变量。当用户输入的温度或压力不合法时,通过它显示错误信息。该字段不参与记录保存,仅用于用户反馈。

表单页面的典型数据流如下:

输入框变化 → 更新 @State → 点击保存 → 生成业务对象 → 刷新列表

5. records:记录列表的数据源

@State records: InjectionDebugRecord[] = [
  new InjectionDebugRecord(
    1,
    'IM-120T-01',
    '外壳件 A01',
    'MOLD-A01',
    'ABS',
    230,
    65,
    85,
    58,
    32,
    '调试正常,产品表面无明显缺陷',
    '2026-07-20 09:30'
  )
]

records 是调试记录列表的数据源,是一个 InjectionDebugRecord 类型的数组。

此处预置了一条演示数据,确保页面打开即可看到列表效果。教程写作中这一点很重要——空白页面不利于读者理解最终呈现形态。

真实项目中,这些记录可能来自本地数据库、后端接口或设备调试历史文件。本文使用内存数组演示,旨在将注意力集中在 ArkTS 页面状态管理与 ArkUI 渲染逻辑上。

6. validateForm 与 toNumber:保存前的校验与转换

校验、正数判断和转换的完整实现见篇末,本节集中说明三者的调用顺序。

这部分代码分为两步:先校验,再转换

validateForm() 负责判断表单是否满足保存条件。料筒温度、模具温度、注射压力、保压压力、成型周期等关键参数,若为空、非数字或小于等于 0,均不应继续保存。

isPositiveNumber() 专注一件事:判断输入值是否为有效正数。将校验逻辑抽离为独立方法,避免散落在 addRecord() 中,后续新增参数校验时也可直接复用。

toNumber() 则只负责类型转换。这里遵循一个设计原则:转换失败时不静默返回 0。对注塑工艺参数而言,0℃0MPa 都会造成记录失真。更稳妥的做法是先拦截错误输入,确认数据有效后再执行转换。

这段代码体现了一个重要的工程习惯:表单提交不等于流程结束,关键生产参数必须先校验、再落库。

7. addRecord:点击按钮后的记录生成逻辑

addRecord() 的完整代码保留在篇末,下面按校验、装配和数组更新三步解释。

这是整篇文章中最核心的业务逻辑。

第一步:表单校验。代码先执行 validateForm(),若关键参数不合法则直接 return,不生成错误记录。这个判断至关重要——调机参数属于生产数据,宁可提示用户重填,也不能将错误值写入记录。

第二步:创建记录对象。通过 new InjectionDebugRecord(...) 实例化新记录,前几个字段直接取自表单状态,温度、压力、周期等字段经 toNumber() 转换为数字类型。

第三步:更新列表数据

this.records = [record, ...this.records]

将新记录置于数组最前端,旧记录依次拼接在后。这样用户刚保存的记录自动出现在列表顶部,符合"最近记录优先"的阅读习惯。

此处未使用 push(),而是重新赋值整个数组——写法更直观,也更易触发页面刷新。这是 ArkTS 状态驱动开发中的常用模式。

8. build:Scroll 包裹完整页面结构

build() {
  Scroll() {
    Column() {
      this.Header()
      this.FormArea()
      this.RecordList()
    }
    .width('100%')
    .padding(16)
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F5F7FA')
}

build() 是页面的渲染入口,页面所有可见元素都从这里开始组织。

外层使用 Scroll,原因在于调机表单字段较多。手机屏幕高度有限,若仅用 Column,底部记录列表可能被挤出可视区域。加上 Scroll 后页面可上下滚动,体验更接近真实 App。

Column 按用户操作顺序组合页面:

  1. 用户先从 Header() 确认页面用途。
  2. 用户再到 FormArea() 填写工艺数据。
  3. 保存后由 RecordList() 展示最新结果。

因此阅读 build() 时,可以直接还原用户的操作路径。

9. Header:顶部标题区

@Builder
Header() {
  Column() {
    Text('注塑机调试记录')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1F2937')
    Text('记录每一次调机参数,方便后续复盘和复用')
      .fontSize(14)
      .fontColor('#6B7280')
      .margin({ top: 6 })
  }
}

Header() 是一个自定义构建函数,负责渲染页面标题区域。

大标题明确告知用户当前页面为"注塑机调试记录",副标题补充说明页面用途——记录调机参数,便于复盘与复用。

工业类 App 的页面文案宜简洁直接。工程师打开页面是为了高效完成记录与查询,标题清晰、说明到位即可。

10. FormArea:集中管理调机参数表单

@Builder
FormArea() {
  Column() {
    Text('本次调试参数')
    this.InputItem('机台编号', this.machineNo, (value: string) => {
      this.machineNo = value
    })
    this.InputItem('产品名称', this.productName, (value: string) => {
      this.productName = value
    })
    Button('保存调机记录')
      .onClick(() => {
        this.addRecord()
      })
  }
}

完整示例中表单区代码较长,以上仅截取核心结构。

表单区的设计思路:每个输入项均调用 InputItem(),传入标签、当前值和变更回调。以机台编号为例,显示 this.machineNo,用户修改后执行:

this.machineNo = value

由此实现输入框与状态变量的双向联动。

表单字段与状态变量的对应关系如下表:

输入沿着“界面值 → 状态 → 记录字段”移动:

  • 机台编号写入 machineNo
  • 产品名称写入 productName
  • 模具编号写入 moldNo
  • 材料名称写入 material
  • 料筒温度校验后进入 barrelTemp
  • 模具温度校验后进入 moldTemp
  • 注射压力进入 injectionPressure
  • 保压压力进入 holdingPressure
  • 成型周期进入 cycleTime
  • 调试结论进入 result,当前不做枚举限制。
    所有映射在 addRecord() 中汇合。

底部按钮绑定 onClick() 事件,用户点击时执行 this.addRecord(),将表单内容存入 records 数组。

这也是业务表单页面的基础范式:多个输入项采集数据,单个提交按钮触发保存。

11. InputItem:抽取重复输入框为统一组件

@Builder
InputItem(label: string, value: string, onChange: (value: string) => void) {
  Column() {
    Text(label)
      .fontSize(13)
      .fontColor('#4B5563')
      .margin({ bottom: 6 })
    TextInput({ text: value, placeholder: `请输入${label}` })
      .height(40)
      .fontSize(14)
      .backgroundColor('#F9FAFB')
      .borderRadius(6)
      .onChange((inputValue: string) => {
        onChange(inputValue)
      })
  }
}

InputItem() 的封装非常关键,它大幅减少了表单代码的重复量。

如果不做封装,每个输入框都要重复编写 TextTextInput、高度、字号、背景色、圆角及 onChange() 逻辑。字段少时问题不大,字段增多后代码维护成本会急剧上升。

抽离为 InputItem() 后,表单区只需关注业务字段本身:

this.InputItem('材料', this.material, (value: string) => {
  this.material = value
})

一行代码同时表达了三件事:标签名称、当前值来源、变更后更新目标。

这就是封装的价值——不是为了炫技,而是让业务代码更聚焦、更易读。

12. RecordList:根据 records 渲染记录列表

@Builder
RecordList() {
  Column() {
    Row() {
      Text('最近调试记录')
      Blank()
      Text(`${this.records.length}`)
    }
    ForEach(this.records, (item: InjectionDebugRecord) => {
      this.RecordCard(item)
    }, (item: InjectionDebugRecord) => item.id.toString())
  }
}

RecordList() 负责渲染最近调试记录区域。

顶部 Row 左侧为标题,右侧为记录总数。Blank() 组件自动填充中间剩余空间,使标题与数量自然分布于两侧。

下方 ForEach 遍历 records 数组,有几条记录就渲染几张 RecordCard 卡片。

第三个参数:

(item: InjectionDebugRecord) => item.id.toString()

用于提供唯一 key。列表渲染时,唯一 key 帮助框架精准识别每一项,减少不必要的重渲染,也避免数据更新后列表项对应关系错乱。

13. RecordCard:单条调试记录的卡片展示

@Builder
RecordCard(item: InjectionDebugRecord) {
  Column() {
    Row() {
      Text(item.machineNo)
      Blank()
      Text(item.createdAt)
    }
    Text(`${item.productName} / ${item.moldNo} / ${item.material}`)
    Text(`料筒 ${item.barrelTemp}℃,模温 ${item.moldTemp}℃,注射 ${item.injectionPressure}MPa,保压 ${item.holdingPressure}MPa,周期 ${item.cycleTime}s`)
    Text(item.result)
  }
}

RecordCard() 接收一条调试记录,将其渲染为信息卡片。

  • 第一行:机台编号 + 记录时间。工程师可快速定位记录来源与时间点。
  • 第二行:产品 / 模具 / 材料。三者经常联动查阅——同一机台可能生产不同产品,同一产品也可能搭配不同模具和材料。
  • 第三行:核心工艺参数汇总。料温、模温、注射压力、保压压力、成型周期集中展示,便于快速扫读。

若后续参数继续增加,不建议全部挤在一行。可改为两行展示,或使用 Grid 布局做成参数宫格,兼顾扫读效率与卡片整洁度。

  • 最后一行:调试结果,如"调试正常"“轻微缩水”"飞边待调整"等。后续可扩展为颜色标签:正常用绿色、待调整用橙色、异常用红色。

14. 整体数据流梳理

至此,完整的代码流程已经清晰:

用户填写 TextInput
        ↓
onChange 更新 @State 变量
        ↓
点击保存按钮
        ↓
addRecord 创建 InjectionDebugRecord
        ↓
records 数组重新赋值
        ↓
ForEach 重新渲染列表
        ↓
RecordCard 展示最新调机记录

这是 ArkTS 页面开发中极为典型的一条数据链路。掌握这条链路后,后续开发生产记录、异常记录、产品档案等模块,思路都是相通的:先设计数据模型,再用状态承接输入,最后将状态映射为界面。

五、UI 细节与工业场景适配

工业 App 用户通常带着明确任务进入页面,因此界面应清晰、直接、少干扰。

本页面的 UI 设计遵循以下原则:

  1. 标题区明确:第一时间告知用户当前页面功能
  2. 分层视觉设计:表单区采用白底卡片,与浅灰背景形成明确分层
  3. 卡片式列表:每条记录独立成卡,便于逐条浏览
  4. 参数集中展示:数值类内容汇总呈现,支持快速扫读
  5. 结果字段突出:调试结果单独展示,便于后续扩展颜色标签

后续可继续优化的方向包括:异常结果颜色标签、空状态提示、表单分组标题、记录卡片的编辑与详情按钮等。

页面采用浅灰背景、白色卡片、蓝色主按钮和深色文字。重点是参数准确与保存反馈,而不是装饰效果。

六、效率对比:数字化记录 vs 传统方式

这个页面之所以值得专门成文,不仅在于功能可运行,更在于它能切实提升现场记录效率。

记录方式 记录速度 复盘难度 历史参数查询
纸质记录 困难
Excel 表格 中等 中等 一般
App 数字化记录 便捷

数字化记录的优势体现在:

  1. 输入项固定,减少漏填错填
  2. 历史记录自动沉淀,随时可查
  3. 后续可扩展筛选、搜索与统计功能
  4. 多人按统一规范记录,数据格式一致

七、常见问题 FAQ

Q1:点击保存后列表没有变化怎么办?

先检查保存方法中是否真正修改了 records 数组。推荐写法:

this.records = [record, ...this.records]

如果仅创建了 record 对象但未放入数组,页面当然不会刷新。

Q2:输入的数字为什么先用 string 类型存储?

TextInput 接收的是文本输入,因此页面输入阶段用 string 存储更便捷。

但生产参数不能始终以字符串处理。保存前需先校验、再转数字。本文通过 validateForm() 校验关键参数,通过 toNumber() 执行最终转换——兼顾输入处理的灵活性与数据写入的准确性。

Q3:真实项目中这些数据应该存储在哪里?

本文先放在页面状态中,便于理解 UI 交互与状态刷新机制。

生产版本需要继续补齐数据能力:

  • 先把页面数据写入可靠存储。
  • 再提供按设备检索记录的入口。
  • 产品与模具可关联已有档案。
  • 报表导出应建立在可信数据上。

这些方向会复用本篇的数据模型。

八、把数据链路真正吃透

前面的代码已经能够形成完整页面,这一节不再增加功能,而是用一次具体操作追踪数据如何移动。假设工程师把料筒温度改为 235TextInput 接收到的仍是字符串;它触发 onChange,再把文本写入由 @State 修饰的 barrelTemp。ArkUI 会跟踪界面对该状态的依赖,因此输入框能够显示新值,但此时还没有创建调试记录。

用户点击保存后,事件进入 addRecord()。方法不会立即执行数字转换,而是先调用 validateForm() 检查五个数值字段;任一字段不合法,流程都会提前返回,并通过 errorMessage 显示反馈。只有校验全部通过,toNumber() 才把字符串转换为数字,随后由 new InjectionDebugRecord(...) 组装业务对象。

构造参数的顺序必须与字段定义保持一致。顺序错位不一定立即产生语法错误,却会让温度、压力或周期进入错误字段,因此调整模型时要同时核对构造方法与调用位置。对象创建完成后,页面仍不会自动出现新卡片,真正触发业务列表变化的是 this.records = [record, ...this.records]

这次赋值创建了一个新数组,把新记录放在首位,再把旧记录依次接到后面。RecordList() 读取更新后的 recordsForEach 按数组顺序生成 RecordCard,一次保存操作至此才形成“表单输入 → 记录对象 → 列表展示”的完整闭环。

排查保存问题时,可以按下面六个检查点从输入端逐步向列表端确认:

  • 检查点一:输入框是否触发 onChange
  • 检查点二:目标 @State 是否收到新值。
  • 检查点三:validateForm() 是否允许继续。
  • 检查点四:字符串是否正确转为数字。
  • 检查点五:新对象字段是否装配正确。
  • 检查点六:records 是否完成重新赋值。

不要一开始就把问题归因于 ForEach。如果状态从未更新、校验提前返回,或者对象创建后没有写入 records,列表都不会出现新数据;按链路逐段验证,比同时修改多个方法更容易找到根因。

九、字段设计为什么这样分组

理解字段用途比记住字段顺序更重要。当前模型可以分为设备定位、产品模具、材料、工艺参数、结果时间和唯一标识六组,每组都回答现场复盘中的一类问题。

1. 设备定位字段

machineNo 标识本次使用的注塑机,回答“这次调试发生在哪台设备上”。演示值 IM-120T-01 便于直接观察页面;生产环境更适合从机台档案中选择,避免同一设备因为人工输入而出现多种编号写法。本文尚未实现机台选择器,这属于后续档案模块的职责。

2. 产品与模具字段

productName 说明本次加工对象,moldNo 说明采用哪套模具。两者不能简单合并,因为同一产品可能使用不同模具版本,而同一模具也可能经历维修或改版。当前示例使用名称和编号保持直观,生产系统则可以进一步保存产品 ID 与模具档案 ID。

3. 材料字段

material 记录原材料名称,因为材料变化会直接影响温度、压力和周期的组合。教程使用 ABS 作为演示值;真实系统还可能需要牌号、供应商和批次,但这些扩展不会改变本篇“输入、校验、创建记录、刷新列表”的数据链路。

4. 工艺数值字段

barrelTempmoldTempinjectionPressureholdingPressurecycleTime 构成核心工艺参数。它们在表单阶段以字符串保存,写入记录前才转换为数字,这样既适配 TextInput,又便于后续排序、统计和范围校验。

当前校验只要求数值大于零,并不代表参数一定符合工艺要求。不同机台、材料和模具的合理范围可能不同,生产规则应来自设备能力或工艺配置,而不是把一套固定上下限写死在页面中。

5. 结果与时间字段

result 保存本次调试结论。自由文本适合快速记录缩水、飞边等现场现象,但不利于稳定统计;生产系统可以使用“结果枚举 + 补充备注”的组合,在保留表达空间的同时获得结构化数据。

createdAt 用于显示记录时间,示例新增记录写入“刚刚”,适合即时反馈,却不适合作为长期存储值。持久化时应保存可排序的标准时间,再由显示层转换为友好文本。

6. 唯一标识字段

id 用于区分不同记录,ForEach 再把它转换为稳定的字符串 key。示例使用 Date.now() 生成 ID,适合单机低频演示;高并发或跨设备写入时应改用数据库主键或 UUID。后者只是生产化建议,当前代码并未实现。

十、状态管理与刷新机制

1. @State 解决的问题

普通字段可以保存数据,但它的变化不一定驱动界面更新。@State 把字段纳入组件状态管理:组件渲染时读取状态,框架据此建立状态与 UI 的依赖,状态变化后再更新相关界面。开发者不需要逐个查找并修改 Text,这正是声明式 UI 的基本思路。

2. 表单状态与业务状态

machineNo 等字段属于表单状态,描述用户当前正在编辑的内容;records 属于业务列表状态,保存已经提交的记录;errorMessage 属于反馈状态,说明最近一次校验为什么失败。三类状态职责不同,本篇分别声明,便于初学者从界面输入一路追踪到业务结果。

3. 为什么重新赋值数组

当前代码没有调用 push(),而是创建一个同时包含新旧数据的数组引用。这样既能直观看到最新记录位于开头,也让状态变化更容易观察。若改成原地修改数组,需要结合项目所用状态管理机制确认观察规则;本文只解释当前源码采用的重新赋值方式。

4. 列表 key 的作用

ForEach 的第三个参数根据记录 id 生成 key。key 应在列表生命周期内保持稳定,而且不能在两条记录之间重复;否则框架可能复用错误节点,表现为内容错位或更新异常。排查列表问题时,除了检查数组长度,也要确认 ID 与 key 是否可靠。

5. 状态更新的调试方法

调试状态链路时,先观察输入框是否显示新值,再检查保存前字段、validateForm() 返回值、新对象内容和 records.length。每次只验证一个环节,比同时修改多处代码更可靠。验证结束后应删除临时输出,也不要把敏感生产参数写入公开日志。

十一、表单校验的设计边界

1. 空字符串

trim() 用于排除纯空格输入,否则空格可能被误当成有效内容。当前正数校验已经覆盖空数值,但文本字段尚未执行同等校验,因此机台编号为空时仍可能创建记录。生产版本应为必要文本字段增加明确规则。

2. 非数字文本

Number(value) 尝试把文本转换为数字,失败时返回 NaN,再由 Number.isNaN(result) 识别。NaN 不能进入工艺记录,否则后续比较、统计和范围判断都可能产生错误结果。

3. 零和负数

当前规则要求数值大于零,可以挡住零值和负数,却不能证明参数符合工艺要求。例如过高温度仍会通过当前校验,生产系统需要按字段配置范围,并允许规则随材料、机台或模具变化。

4. 小数输入

Number() 可以解析合法小数,因此 32.5 能够转换为数字。是否允许小数以及保留几位精度,应由具体工艺规则决定;本文没有限制小数位数,生产系统可在保存前统一精度。

5. 错误提示顺序

validateForm() 按字段顺序检查,并且一次只显示第一个错误。这样的实现直接、提示清晰,但用户可能需要多次提交才能修正全部问题。另一种方案是收集所有错误后统一展示,不过它会增加状态结构和界面复杂度,本篇暂时采用单错误提示。

6. 校验通过后的状态

校验成功时必须清空 errorMessage,否则旧错误会继续留在页面。扩展校验分支时既要检查失败提示,也要确认成功路径仍然能够恢复正常状态。

十二、逐步定位常见故障

故障一:输入框无法修改

  • 检查 TextInput 是否绑定当前状态。
  • 检查 onChange 是否接收新值。
  • 检查回调是否写回正确字段。
  • 检查是否误写到其他同名变量。

故障二:点击保存没有反应

  • 检查按钮是否绑定 onClick
  • 检查事件中是否调用 addRecord()
  • 检查 validateForm() 是否提前返回。
  • 检查页面是否显示 errorMessage

故障三:数值转换后异常

  • 检查输入中是否包含单位文本。
  • 输入框只应保存数字字符串。
  • 单位应由界面固定展示。
  • 检查小数点是否符合解析格式。
  • 检查是否绕过了正数校验。

故障四:记录生成但列表不更新

  • 检查 records.length 是否增加。
  • 检查是否重新赋值给 records
  • 检查 records 是否带有 @State
  • 检查 RecordList() 是否读取同一数组。
  • 检查新记录是否位于数组首项。

故障五:列表内容错位

  • 检查每条记录的 id 是否唯一。
  • 检查 key 是否来自稳定字段。
  • 检查构造参数顺序是否错位。
  • 检查卡片读取的字段是否正确。

故障六:重新打开应用后数据消失

  • 当前记录只存在页面内存中。
  • 页面示例没有执行持久化。
  • 退出应用后丢失属于预期边界。
  • 下一阶段可接入本地存储。
  • 接入前应先设计数据版本迁移。

故障七:错误信息一直存在

  • 检查成功校验是否清空错误。
  • 检查新增分支是否遗漏清理。
  • 检查输入变化时是否需要即时清除。
  • 本文选择保存成功后统一清除。

十三、从演示页面走向生产模块

当前页面适合学习基础数据链路,但它还不是可以直接投入生产的完整模块。实际落地前,需要继续补齐存储、档案、校验、身份、时间、附件、审计、性能和安全等能力。

1. 数据持久化

页面内存不能承担长期记录,需要根据部署方式选择本地数据库或后端服务。离线车间还要设计同步策略,并在写入失败时明确反馈,不能先显示保存成功再悄悄丢失数据。

2. 档案关联

机台、产品和模具最好来自对应档案,而不是完全依赖自由文本。业务记录保存稳定的关联 ID,界面继续展示名称和编号,这样既便于用户阅读,也更适合后续统计。

3. 参数范围

不同机台具有不同能力边界,不同材料也有各自的加工窗口,因此统一正数校验只能挡住明显错误。生产规则应支持动态范围,并根据风险把越界结果区分为提示、警告或禁止保存。

4. 操作身份

调试记录通常需要操作人员、班次,某些流程还需要审核人。身份信息不应由用户随意填写,而应来自登录状态、组织信息或排班数据。

5. 时间与时区

文本“刚刚”只适合即时展示,持久化时应保存标准时间值。涉及多设备或跨地区同步时还要统一时区策略,再由显示层生成友好的本地时间文本。

6. 异常与附件

结果字段可以扩展异常分类,并关联现场照片或检测报告。附件上传需要处理失败、重试和断点场景,敏感生产资料还必须配置访问控制。

7. 编辑与审计

生产记录可以允许更正,但不应覆盖历史痕迹。系统可记录修改人、时间、原因与变更前内容,具体审计强度取决于企业管理制度。

8. 性能与分页

少量内存记录可以直接渲染,长期积累后则需要分页或分段加载。筛选应尽量在数据层完成,避免把全部历史记录一次性放入页面。

9. 安全与隐私

日志不应输出完整生产参数,接口需要鉴权与传输保护,导出文件也要限制访问范围。本文没有实现这些生产能力,因此不能把演示页面直接视为生产方案。

十四、完整代码

以下是该页面的完整示例代码:

class InjectionDebugRecord {
  id: number
  machineNo: string
  productName: string
  moldNo: string
  material: string
  barrelTemp: number
  moldTemp: number
  injectionPressure: number
  holdingPressure: number
  cycleTime: number
  result: string
  createdAt: string

  constructor(
    id: number,
    machineNo: string,
    productName: string,
    moldNo: string,
    material: string,
    barrelTemp: number,
    moldTemp: number,
    injectionPressure: number,
    holdingPressure: number,
    cycleTime: number,
    result: string,
    createdAt: string
  ) {
    this.id = id
    this.machineNo = machineNo
    this.productName = productName
    this.moldNo = moldNo
    this.material = material
    this.barrelTemp = barrelTemp
    this.moldTemp = moldTemp
    this.injectionPressure = injectionPressure
    this.holdingPressure = holdingPressure
    this.cycleTime = cycleTime
    this.result = result
    this.createdAt = createdAt
  }
}

@Entry
@Component
struct InjectionDebugPage {
  @State machineNo: string = 'IM-120T-01'
  @State productName: string = '外壳件 A01'
  @State moldNo: string = 'MOLD-A01'
  @State material: string = 'ABS'
  @State barrelTemp: string = '230'
  @State moldTemp: string = '65'
  @State injectionPressure: string = '85'
  @State holdingPressure: string = '58'
  @State cycleTime: string = '32'
  @State result: string = '调试正常'
  @State errorMessage: string = ''

  @State records: InjectionDebugRecord[] = [
    new InjectionDebugRecord(
      1,
      'IM-120T-01',
      '外壳件 A01',
      'MOLD-A01',
      'ABS',
      230,
      65,
      85,
      58,
      32,
      '调试正常,产品表面无明显缺陷',
      '2026-07-20 09:30'
    )
  ]

  private validateForm(): boolean {
    if (!this.isPositiveNumber(this.barrelTemp)) {
      this.errorMessage = '料筒温度需要填写有效数字'
      return false
    }
    if (!this.isPositiveNumber(this.moldTemp)) {
      this.errorMessage = '模具温度需要填写有效数字'
      return false
    }
    if (!this.isPositiveNumber(this.injectionPressure)) {
      this.errorMessage = '注射压力需要填写有效数字'
      return false
    }
    if (!this.isPositiveNumber(this.holdingPressure)) {
      this.errorMessage = '保压压力需要填写有效数字'
      return false
    }
    if (!this.isPositiveNumber(this.cycleTime)) {
      this.errorMessage = '成型周期需要填写有效数字'
      return false
    }
    this.errorMessage = ''
    return true
  }

  private isPositiveNumber(value: string): boolean {
    const result = Number(value)
    return value.trim().length > 0 && !Number.isNaN(result) && result > 0
  }

  private toNumber(value: string): number {
    return Number(value)
  }

  private addRecord(): void {
    if (!this.validateForm()) {
      return
    }
    const record = new InjectionDebugRecord(
      Date.now(),
      this.machineNo,
      this.productName,
      this.moldNo,
      this.material,
      this.toNumber(this.barrelTemp),
      this.toNumber(this.moldTemp),
      this.toNumber(this.injectionPressure),
      this.toNumber(this.holdingPressure),
      this.toNumber(this.cycleTime),
      this.result,
      '刚刚'
    )
    this.records = [record, ...this.records]
  }

  build() {
    Scroll() {
      Column() {
        this.Header()
        this.FormArea()
        this.RecordList()
      }
      .width('100%')
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F7FA')
  }

  @Builder
  Header() {
    Column() {
      Text('注塑机调试记录')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1F2937')
      Text('记录每一次调机参数,方便后续复盘和复用')
        .fontSize(14)
        .fontColor('#6B7280')
        .margin({ top: 6 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .margin({ bottom: 16 })
  }

  @Builder
  FormArea() {
    Column() {
      Text('本次调试参数')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#111827')
        .margin({ bottom: 12 })

      this.InputItem('机台编号', this.machineNo, (value: string) => {
        this.machineNo = value
      })
      this.InputItem('产品名称', this.productName, (value: string) => {
        this.productName = value
      })
      this.InputItem('模具编号', this.moldNo, (value: string) => {
        this.moldNo = value
      })
      this.InputItem('材料', this.material, (value: string) => {
        this.material = value
      })
      this.InputItem('料筒温度', this.barrelTemp, (value: string) => {
        this.barrelTemp = value
      })
      this.InputItem('模具温度', this.moldTemp, (value: string) => {
        this.moldTemp = value
      })
      this.InputItem('注射压力', this.injectionPressure, (value: string) => {
        this.injectionPressure = value
      })
      this.InputItem('保压压力', this.holdingPressure, (value: string) => {
        this.holdingPressure = value
      })
      this.InputItem('成型周期', this.cycleTime, (value: string) => {
        this.cycleTime = value
      })
      this.InputItem('调试结果', this.result, (value: string) => {
        this.result = value
      })

      if (this.errorMessage.length > 0) {
        Text(this.errorMessage)
          .fontSize(13)
          .fontColor('#DC2626')
          .margin({ top: 4, bottom: 4 })
      }

      Button('保存调机记录')
        .width('100%')
        .height(44)
        .margin({ top: 12 })
        .backgroundColor('#2563EB')
        .fontColor(Color.White)
        .onClick(() => {
          this.addRecord()
        })
    }
    .width('100%')
    .padding(16)
    .borderRadius(8)
    .backgroundColor(Color.White)
    .margin({ bottom: 16 })
  }

  @Builder
  InputItem(label: string, value: string, onChange: (value: string) => void) {
    Column() {
      Text(label)
        .fontSize(13)
        .fontColor('#4B5563')
        .margin({ bottom: 6 })
      TextInput({ text: value, placeholder: `请输入${label}` })
        .height(40)
        .fontSize(14)
        .backgroundColor('#F9FAFB')
        .borderRadius(6)
        .onChange((inputValue: string) => {
          onChange(inputValue)
        })
    }
    .width('100%')
    .margin({ bottom: 10 })
  }

  @Builder
  RecordList() {
    Column() {
      Row() {
        Text('最近调试记录')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .fontColor('#111827')
        Blank()
        Text(`${this.records.length}`)
          .fontSize(13)
          .fontColor('#2563EB')
      }
      .width('100%')
      .margin({ bottom: 12 })

      ForEach(this.records, (item: InjectionDebugRecord) => {
        this.RecordCard(item)
      }, (item: InjectionDebugRecord) => item.id.toString())
    }
    .width('100%')
  }

  @Builder
  RecordCard(item: InjectionDebugRecord) {
    Column() {
      Row() {
        Text(item.machineNo)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#111827')
        Blank()
        Text(item.createdAt)
          .fontSize(12)
          .fontColor('#6B7280')
      }
      .width('100%')

      Text(`${item.productName} / ${item.moldNo} / ${item.material}`)
        .fontSize(13)
        .fontColor('#4B5563')
        .margin({ top: 6 })

      Text(`料筒 ${item.barrelTemp}℃,模温 ${item.moldTemp}℃,注射 ${item.injectionPressure}MPa,保压 ${item.holdingPressure}MPa,周期 ${item.cycleTime}s`)
        .fontSize(13)
        .fontColor('#374151')
        .margin({ top: 8 })

      Text(item.result)
        .fontSize(13)
        .fontColor('#059669')
        .margin({ top: 8 })
    }
    .width('100%')
    .padding(14)
    .borderRadius(8)
    .backgroundColor(Color.White)
    .margin({ bottom: 10 })
  }
}

十五、本篇小结与系列衔接

本文完成了"注塑工程师助手"的第一个核心页面——注塑机调试记录。

通过本篇学习,你已掌握:

  1. 如何定义调机记录的数据模型
  2. 如何用 ArkUI 搭建工艺参数录入表单
  3. 如何用 @State 管理页面输入状态
  4. 如何通过按钮点击生成新记录
  5. 如何用 ForEach 渲染调试记录列表

这个页面虽然功能精简,但已具备真实业务 App 的基本形态。下一篇我们将继续扩展"注塑机档案列表",管理机台编号、吨位、品牌、车间位置及运行状态,为后续生产记录与调机历史功能打下基础。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐