前言模板不是一组孤立的版本字符串在新建调机记录时加入了最近参数复制。复制动作解决的是输入效率但复制出来的参数仍然需要经过调机、试样和复核不能直接被当成正式模板。M3 继续向前推进后工程师需要在“调机记录”和“参数模板”之间建立更清楚的边界哪一个对象代表候选版本哪一个对象代表已经应用的版本应用到了哪台机台以及应用过程如何留下记录。当前工程没有把参数模板设计成一个只有currentVersion和candidateVersion的页面变量而是使用ParameterTemplate模型和DemoBusinessRepository统一管理。模板列表负责让用户选择对象模板详情负责比较和应用Repository 负责查找、修改和快照恢复。本篇围绕这些真实代码解释数据边界并说明当前演示实现没有覆盖哪些生产能力。一、先把调机记录和参数模板分开调机记录描述一次具体的现场动作和试样结果参数模板描述一组可以被确认和应用的版本关系。两者可能有关联但不能互相替代。对象主要回答的问题当前模型调机记录这次调了什么参数试样结果怎样DebugRecord参数模板当前版本和候选版本是什么已经应用到哪里ParameterTemplate产品参数模板属于哪个产品Product.templateId机台模板最终应用到哪一台设备ParameterTemplate.appliedMachineId如果把所有字段都塞到调机记录里页面很快会同时承担试验记录、版本审批和应用追踪后续很难判断某个版本是不是已经生效。当前工程通过独立模型把这些概念拆开页面之间只传递对象 ID不复制整份对象。二、ParameterTemplate 的字段各自负责什么模型文件harmonyos-app/entry/src/main/ets/models/ParameterTemplate.ets的核心定义是exporttypeTemplateStatuscandidate|applied|rolled_back;exportclassParameterTemplate{id:string;productId:string;currentVersion:string;candidateVersion:string;appliedMachineId:string;status:TemplateStatus;applicationLog:string[];constructor(id:string,productId:string,currentVersion:string,candidateVersion:string,appliedMachineId:string,status:TemplateStatus,applicationLog:string[]){this.idid;this.productIdproductId;this.currentVersioncurrentVersion;this.candidateVersioncandidateVersion;this.appliedMachineIdappliedMachineId;this.statusstatus;this.applicationLogapplicationLog;}}这些字段不是重复存储同一个事实。id是模板的稳定身份列表 key 和详情查找都依赖它。productId表示模板属于哪个产品页面可以据此显示产品名称。currentVersion表示当前版本回答“现在使用的是什么”。candidateVersion表示待确认版本回答“准备应用什么”。appliedMachineId记录最近一次应用到的机台没有应用时可以是空字符串。status是流程状态当前用联合类型限制为候选、已应用和已回滚。applicationLog保存应用动作的文字记录当前是内存数组。初学者容易把status和currentVersion candidateVersion当成一回事。它们表达的层次不同版本字段说明版本关系状态字段说明流程阶段。应用动作发生后两个版本可能相同但仍需要状态字段告诉页面这个对象已经完成应用。三、联合类型先收紧状态边界TemplateStatus不是普通的stringexporttypeTemplateStatuscandidate|applied|rolled_back;这样做的直接收益是创建模板和恢复快照时只能传入约定的状态值。拼写错误、随意使用中文文案或把异常状态塞进模型会在类型检查阶段暴露而不是等到列表颜色或详情分支运行时才发现。当前页面实际使用了candidate和applied两种状态rolled_back已经被模型保留为扩展边界但TemplateDetail.ets当前没有实现回滚按钮。文章只解释模型允许的范围不把预留状态写成已经完成的回滚流程。四、Repository 是模板的单一事实来源DemoBusinessRepository当前初始化了两条脱敏模板数据privatetemplates:ParameterTemplate[][newParameterTemplate(template-c08,product-c08,v2.3,v2.4,machine-001,candidate,[]),newParameterTemplate(template-a7,product-a7,v1.8,v1.9,machine-002,applied,[v1.9 已应用至 IM-220T-03])];这两条数据覆盖了当前页面最重要的两种展示状态C08 密封盖有候选版本v2.4A7 阀体上盖已有应用版本v1.9。模板列表和模板详情都通过 Repository 读取它们而不是各自写一份数组。Repository 对外提供的读取方法保持简单loadTemplates():ParameterTemplate[]{returnthis.templates;}templateById(id:string):ParameterTemplate|undefined{returnthis.templates.find((item:ParameterTemplate)item.idid);}loadTemplates()服务于列表templateById()服务于详情。返回类型包含undefined提醒调用方传入的 ID 可能已经不存在不能把查找结果直接当成确定对象使用。五、模板列表只负责扫读和传递 IDTemplateList.ets通过一个方法把模型转成列表摘要privatelabel(template:ParameterTemplate):string{constproduct:Product|undefineddemoBusinessRepository.productById(template.productId);constproductName:stringproductundefined?template.productId:product.name;conststate:stringtemplate.statusapplied?已应用:候选版本;return${productName}\n当前${template.currentVersion}· 候选${template.candidateVersion}·${state};}这里把产品 ID 转换成用户能读懂的产品名称但没有把模板对象复制到页面状态中。产品找不到时回退显示productId这是一种可观察的降级结果比直接访问product.name导致页面异常更容易排查。列表使用稳定的模板 ID 作为 key并把 ID 传给外部回调ForEach(demoBusinessRepository.loadTemplates(),(template:ParameterTemplate){Button(this.label(template)).width(100%).height(78).onClick(()this.onOpenTemplate(template.id));},(template:ParameterTemplate)template.id)列表不知道详情页如何路由也不负责应用模板。它只完成三件事读取列表、渲染摘要、传出被点击对象的 ID。这个边界与调机记录列表保持一致页面入口统一装配详情组件。六、详情页把版本比较和应用动作放在一起模板详情页通过templateId查找当前对象templateId:string;StateselectedMachineId:string;Statefeedback:string;aboutToAppear():void{consttemplate:ParameterTemplate|undefineddemoBusinessRepository.templateById(this.templateId);if(template!undefined)this.selectedMachineIdtemplate.appliedMachineId;}aboutToAppear()只负责把已有的应用机台回填到选择状态不直接执行应用。用户打开详情时应该先看到当前数据再决定是否选择其他机台和确认应用。页面通过current()集中读取模板对象privatecurrent():ParameterTemplate{consttemplate:ParameterTemplate|undefineddemoBusinessRepository.templateById(this.templateId);returntemplateundefined?newParameterTemplate(missing,,-,-,,candidate,[]):template;}这个方法提供了一个安全的显示兜底对象但它不等于把错误 ID 变成真实模板。外层exists()会先判断对象是否存在不存在时展示“未找到参数模板”和返回按钮只有存在时才进入版本比较和应用界面。显示兜底是为了让模板表达式有稳定返回值不能当作保存数据。七、应用机台是模板边界的一部分当前详情页没有一个“直接应用”按钮就结束而是先列出机台Text(选择应用机台)ForEach(demoBusinessRepository.loadMachines(),(machine:Machine){Button(this.machineLabel(machine)).onClick(()this.selectedMachineIdmachine.id)},(machine:Machine)machine.id)machineLabel()把机台 ID 转换为IM-120T-11 · 精密外壳注塑机这样的现场名称。选择状态保存的是机器 ID而不是显示文案这样提交时仍然使用稳定标识文案调整不会破坏关联关系。当前演示页面列出四台机台包括IM-120T-11、IM-220T-03、IM-320T-02和IM-450T-01。这只是当前 Repository 的脱敏样本不意味着任意模板都可以在生产环境应用到任意机台。真正的生产系统还要增加产品、模具、权限和设备能力的匹配校验。八、应用动作如何改变模型详情页的确认动作调用 Repositoryprivateapply():void{if(this.selectedMachineId.length0){this.feedback请选择应用机台后再确认。;return;}constapplied:booleandemoBusinessRepository.applyTemplate(this.templateId,this.selectedMachineId);this.feedbackapplied?模板已应用应用日志已更新。:模板应用失败请返回列表重新选择。;}先判断selectedMachineId是因为页面输入完整性和 Repository 查找成功是两层不同的边界。没有选择机台时应用动作不应该进入数据层模板 ID 不存在时Repository 返回false页面再显示失败反馈。Repository 的应用逻辑是applyTemplate(templateId:string,machineId:string):boolean{consttemplate:ParameterTemplate|undefinedthis.templateById(templateId);if(templateundefined||machineId.length0)returnfalse;template.currentVersiontemplate.candidateVersion;template.statusapplied;template.appliedMachineIdmachineId;template.applicationLog.push(${template.currentVersion}已应用至${machineId});this.notifyStateChanged();returntrue;}这段代码体现了当前演示实现的状态转换候选版本被写入当前版本状态变成applied记录应用机台并追加一条日志。页面只调用方法并显示反馈不重复修改模板字段避免页面和 Repository 各自维护一套转换规则。九、应用日志为什么要跟着对象保存当前日志是applicationLog: string[]应用成功时追加template.applicationLog.push(${template.currentVersion}已应用至${machineId});详情页通过ForEach渲染日志没有日志时显示“暂无应用记录”。这让用户能够区分“模板从未应用”和“模板已经应用过但当前版本没有候选变化”。日志文字当前适合演示页面观察但生产环境不能只依赖一段拼接文本完成追溯。正式实现通常还需要操作者、时间、来源调机记录、目标机台和结果状态等结构化字段。本篇把这些内容作为边界说明不把当前字符串数组夸大成完整审计日志。十、模板、产品和调机记录如何形成关系当前 Repository 中产品通过templateId指向参数模板调机记录则拥有自己的产品和机台字段。这个关系可以帮助页面回答当前产品默认关联哪个模板。模板候选版本属于哪个产品。应用时选择了哪台机台。调机记录是否只是一次试验还是已经进入模板应用链路。调机记录的状态draft、submitted、approved与模板状态candidate、applied、rolled_back并不是同一组枚举。前者描述调机记录的复核过程后者描述模板版本的应用过程。不要为了减少字段而让一个状态字段同时表达两条流程否则“记录已提交”很容易被误读为“模板已应用”。十一、快照恢复也必须遵守模板边界Repository 的快照接口为模板定义了独立结构interfaceTemplateSnapshot{id:string;productId:string;currentVersion:string;candidateVersion:string;appliedMachineId:string;status:TemplateStatus;applicationLog:string[];}创建快照时模板字段被逐项映射到快照恢复时再通过new ParameterTemplate(...)重建模型。这样做的好处是快照结构与运行时对象的职责可以分别审查数组引用也不会直接泄漏到外部。恢复前的校验要求状态仍然属于约定的联合类型privateisTemplateSnapshot(item:TemplateSnapshot):boolean{returnitem!undefineditem!nulltypeofitem.idstringtypeofitem.productIdstringtypeofitem.currentVersionstringtypeofitem.candidateVersionstringtypeofitem.appliedMachineIdstringArray.isArray(item.applicationLog)item.applicationLog.every((entry:string)typeofentrystring)(item.statuscandidate||item.statusapplied||item.statusrolled_back);}当前快照版本是演示用的version: 1只说明已有状态结构的恢复边界不代表已经设计完成迁移、冲突合并或跨设备同步。十二、实际运行观察本次核对在 API 24 模拟器中按以下路径进行打开底部“调机”页签。点击“参数模板”。观察模板列表中的 C08 和 A7 两条记录。打开“C08 密封盖”模板详情。观察版本比较、机台选择和应用日志区域。当前运行结果为模板列表显示“C08 密封盖 / 当前 v2.3 / 候选 v2.4 / 候选版本”。模板列表显示“A7 阀体上盖 / 当前 v1.8 / 候选 v1.9 / 已应用”。C08 详情显示当前版本v2.3和候选版本v2.4。详情页列出四个可选择的机台按钮。C08 没有应用历史时显示“暂无应用记录”。页面提示“候选参数来自已提交的调机记录应用前必须确认机台”。这些观察证明当前模型、列表和详情页的边界可以被用户界面复核。本文没有点击确认应用来改变演示数据因此不把一次未执行的应用动作写成已验证结果。十三、常见排错顺序1. 列表显示产品 ID 而不是产品名称先确认productId是否能在 Repository 的产品集合中查到再检查label()是否使用了查找结果。产品名称是显示层信息不能反过来作为模板关联 ID。2. 打开详情后版本都是短横线检查templateId是否正确传递以及templateById()是否返回undefined。不要直接给current()塞一组看起来真实的默认版本否则会把错误路由伪装成合法模板。3. 点击应用后页面没有变化先确认是否选择了机台再确认模板 ID 存在最后检查applyTemplate()是否修改了状态并调用notifyStateChanged()。页面反馈文字不是数据更新本身必须同时观察版本、状态和日志。4. 应用日志重复出现检查按钮是否被重复点击以及 Repository 是否在应用失败时也追加日志。当前代码只在模板存在且机台 ID 非空时修改模型。5. 把调机记录状态当成模板状态分别查看DebugRecordStatus和TemplateStatus的联合类型。两套状态服务于不同流程不能用中文标签互相比较。十四、演示实现和生产实现的边界当前模板数据来自本地脱敏 Repository版本字段是字符串应用日志也是内存数组。生产环境需要增加版本唯一性、候选来源、操作人、审批权限、设备能力匹配、并发更新和持久化事务等设计。当前applyTemplate()是同步方法返回布尔值表示演示层是否找到模板并完成内存修改。接入网络或数据库后应用动作通常会变成异步请求需要增加加载中、失败重试和重复提交保护。本文不把当前同步实现写成服务端事务。rolled_back已经出现在模型类型中但页面没有回滚入口和运行证据。后续实现回滚时应补充回滚来源、目标版本、操作日志和恢复验证不能仅把状态字符串改回candidate就宣称完成回滚。十五、总结本篇围绕真实 ArkTS 工程建立了参数模板的数据边界用ParameterTemplate区分产品、当前版本、候选版本、应用机台和状态。用TemplateStatus联合类型收紧流程状态。用 Repository 作为模板查询、应用和快照恢复的单一事实来源。让模板列表只负责摘要和传递稳定 ID。让模板详情负责版本比较、机台选择和应用反馈。把调机记录状态与模板应用状态分开避免两条流程互相污染。通过 API 24 模拟器观察 C08 候选版本、A7 已应用版本和空应用日志。附录工程配置与版本说明为了便于读者复现本文中的代码片段和运行现象这里把当前文章系列对应的工程基线单独列出。本文所说的“当前工程”指e_notebook项目的 HarmonyOS ArkTS 客户端应用名称为“注塑工程师助手”主要用于脱敏演示机台档案、产品档案、调机记录、参数模板、异常闭环、生产批次和看板报表等业务路径。1. 应用与模块配置应用包名com.atan.enotebook。应用版本versionName为1.0.0versionCode为1000000。工程模型ArkTS / ArkUI Stage 模型。主模块entry模块类型为entry。入口 AbilityEntryAbility入口文件为entry/src/main/ets/entryability/EntryAbility.ets。主页面配置模块通过pages: $profile:main_pages读取页面列表。设备类型当前模块声明支持phone、tablet和2in1。安装方式deliveryWithInstall为trueinstallationFree为false属于随应用安装的普通 entry 模块。2. SDK 与 API 版本DevEco Studio 版本DevEco Studio Beta26.0.0.461。编译 SDKHarmonyOS SDK API 26 Beta1SDK 包版本为26.0.0.23。SDK 平台信息apiVersion为26platformVersion为26.0.0releaseType/stage为Beta1。targetSdkVersion26.0.0。compatibleSdkVersion6.1.1(24)。API 口径说明文章系列以 API 24 作为兼容目标进行表述当前工程实际由 API 26 Beta SDK 编译并在 API 24 模拟器上做过安装、启动和交互观察。因此文中的“API 24 运行观察”表示兼容目标环境下的模拟器验证结果不等同于使用 API 24 SDK 重新完成编译验证。3. 构建与运行工具开发工具 IDEDevEco Studio Beta安装目录指向D:/Program Files/Huawei/DevEco Studio Beta。SDK 路径D:/Program Files/Huawei/DevEco Studio Beta/sdk。构建系统Hvigor工程入口hvigorfile.ts使用ohos/hvigor-ohos-plugin的appTasks。Hvigor 执行配置开启 daemon、incremental、parallel 和 typeCheck日志级别为info。构建脚本本地build.ps1优先使用 DevEco Studio 自带的 JBR、Node.js、SDK 与 Hvigor避免系统环境变量中的 Java 或 Node.js 版本干扰构建结果。调试产物未配置签名时本地构建生成entry/build/default/outputs/default/entry-default-unsigned.hap。这类 unsigned HAP 只用于本地调试和模拟器验证正式发布前需要在 DevEco Studio 中补充签名配置。4. 本系列文章的验证边界本系列代码以脱敏演示数据为主Repository、Store、页面状态和组件边界都围绕本地演示闭环展开。已观察过的运行现象以文中对应截图、布局树和人工核对记录为准没有重新核对的页面不在单篇文章中扩大为完整结论。如果读者使用更新的 DevEco Studio、HarmonyOS SDK 或真机系统版本复现API 差异、控件行为和签名流程可能会发生变化。遇到差异时建议优先核对build-profile.json5、module.json5、SDK Manager 中安装的 API 版本以及当前设备或模拟器的系统 API 等级。附录 2项目目录结构与设计意图下面这份目录说明对应当前 DevEco Studio 中打开的harmonyos-app工程。截图里能看到的目录并不只是文件摆放习惯它反映了一个 ArkTS Stage 工程的分层方式应用级配置、业务模块、页面源码、资源文件、构建配置和过程归档分别放在不同位置方便后续排查问题时先判断“问题属于配置、页面、数据、状态、资源还是构建产物”。harmonyos-app/ ├── AppScope/ # 应用级配置与全局资源入口 │ ├── app.json5 # bundleName、版本号、图标、应用标签等应用级元信息 │ └── resources/ # 应用级图标、字符串和基础资源 ├── entry/ # 主业务模块当前 App 的主要页面和业务代码都在这里 │ ├── src/main/ets/ # ArkTS 源码根目录 │ │ ├── components/ # 可复用 ArkUI 组件如底部导航、数据状态面板 │ │ ├── entryability/ # Stage 模型入口 Ability负责应用启动入口 │ │ ├── features/ # 按业务域拆分的功能页面 │ │ │ ├── debug/ # 调机记录相关页面 │ │ │ ├── exceptions/ # 异常处置与闭环相关页面 │ │ │ ├── home/ # 首页看板与概览入口 │ │ │ ├── machines/ # 机台档案列表、详情和机台相关交互 │ │ │ ├── production/ # 生产批次、报工和结案门禁相关页面 │ │ │ ├── products/ # 产品档案、产品详情和关联信息 │ │ │ ├── reports/ # 周报、月报、班次报表和下钻入口 │ │ │ └── templates/ # 参数模板列表与详情 │ │ ├── models/ # 业务对象的数据结构如 Machine、Product、DebugRecord │ │ ├── pages/ # 页面容器与导航装配如 Index.ets │ │ ├── repositories/ # 脱敏演示数据、查询方法、快照持久化和数据重置边界 │ │ ├── stores/ # 页面路由、导航选择和共享状态规则 │ │ └── utils/ # 主题令牌、校验函数等通用工具 │ ├── src/main/resources/base/ # 模块级资源目录 │ │ ├── element/ # 字符串、颜色等基础资源声明 │ │ ├── media/ # 图标、启动图等媒体资源 │ │ └── profile/ # 页面 profile 配置如 main_pages.json │ ├── src/main/module.json5 # entry 模块配置声明 EntryAbility、设备类型和页面入口 │ ├── build-profile.json5 # 模块级构建目标、混淆和 target 配置 │ └── oh-package.json5 # entry 模块包信息与依赖声明 ├── hvigor/ # Hvigor 构建系统配置 │ └── hvigor-config.json5 # 构建执行参数如增量、并行和类型检查 ├── build-profile.json5 # 工程级 SDK、targetSdkVersion、compatibleSdkVersion 配置 ├── hvigorfile.ts # 工程级构建任务入口接入 appTasks ├── local.properties # 本机 SDK 路径配置 ├── oh-package.json5 # 工程级包信息与依赖声明 ├── build.ps1 # 本地构建脚本固定使用 DevEco Studio 自带工具链 ├── document_claude/ # 开发过程归档、测试记录和验证材料 ├── .hvigor/ # Hvigor 生成的缓存和构建记录不作为手写源码维护 ├── .idea/ # DevEco Studio / IntelliJ 工程配置不承载业务逻辑 └── entry/build/ # 构建输出目录HAP 和中间产物由构建流程生成1. 为什么应用级配置放在AppScopeAppScope负责应用整体身份而不是某个页面的业务逻辑。app.json5中的bundleName、versionName、versionCode、应用图标和应用标签会影响安装包身份、桌面展示和版本识别。把这类配置放在应用级目录可以避免业务页面为了改一个标题或图标而混入应用发布配置。在当前工程中AppScope更像“应用身份证”。它回答的是“这个 App 是谁、版本是多少、展示什么图标”而不是“机台列表怎么筛选、详情页怎么返回”。2. 为什么业务代码集中在entry/src/main/etsentry是当前工程的主业务模块src/main/ets是 ArkTS 源码根目录。截图里打开的MachineDetail.ets就位于features/machines下面说明机台详情页被归入“机台业务域”而不是随意放在全局页面目录中。这种组织方式的好处是定位明确机台问题优先看features/machines产品问题优先看features/products生产批次问题优先看features/production。当文章里讨论某个业务链路时读者也能从目录直接反推代码位置。3.components、features和pages的边界components放的是可复用组件例如底部导航、加载/空态/失败态面板。它们不应该直接知道“当前打开的是哪台机台”而是通过参数和回调服务于不同页面。features放的是业务域页面。每个子目录都围绕一个业务主题组织例如machines负责机台档案templates负责参数模板exceptions负责异常闭环。业务页面可以组合组件也可以读取模型和仓储但应尽量把本业务域的显示和交互留在本目录内。pages更偏页面容器和入口装配。当前Index.ets承担主页面状态切换、底部导航和详情路径分发等职责。它不应该塞满所有业务细节而是负责把用户当前所在位置、打开对象和页面分支组织起来。4.models、repositories和stores分别解决什么问题models定义数据形状例如机台、产品、调机记录、生产批次等对象有哪些字段。它让页面和仓储使用同一套类型语言避免每个页面临时拼对象。repositories定义数据来源和查询边界。当前工程使用脱敏演示数据和本地持久化快照因此仓储层负责“从哪里取数据、按什么 ID 查询、怎样重置演示数据”。页面不直接关心数据是内置数组、Preferences 快照还是后续真实接口。stores定义页面级或应用级状态规则例如当前导航项、路由分支、打开详情的类型和 ID。把状态规则从具体组件中抽出来可以减少“列表、详情、导航互相覆盖状态”的问题。5. 为什么资源放在resources/baseresources/base/element管字符串、颜色等声明resources/base/media管图标和图片resources/base/profile管页面 profile。它们和 ArkTS 页面代码分开是为了让“界面逻辑”和“静态资源”各自清晰。如果页面显示异常先判断是布局代码问题还是资源引用问题。比如图标不显示应优先检查media和资源引用页面无法进入应检查profile/main_pages.json和module.json5的页面声明颜色或字符串不符合预期则回到element下核对。6. 构建目录和生成目录不要手工维护.hvigor、entry/build和部分中间产物目录由构建系统生成主要用于缓存、编译记录、HAP 输出和临时文件。它们可以帮助排查构建结果但不应该作为手写业务代码维护。当前调试 HAP 位于entry/build/default/outputs/default/entry-default-unsigned.hap。这个路径说明构建已经产出安装包但它仍是 unsigned 调试产物正式发布前应回到 DevEco Studio 的签名配置和发布流程而不是直接修改build目录里的文件。