资讯动态

【健康菜谱助手|02】HarmonyOS ArkTS 烹饪步骤实战:把分步操作、完成状态和返回恢复做稳

发布时间:2026/10/24 2:04:06 来源:尧图企业网站定制
菜谱详情页解决的是“做什么”烹饪步骤页解决的则是“现在做到哪里、下一步是什么、退出后还能不能接着做”。对用户来说切菜时锁屏、接电话后返回、误触上一页、切到步骤总览再回来都是正常行为对工程来说这些行为会同时触发路由、页面生命周期、状态恢复和语音资源管理。一旦只用一个currentStep临时变量支撑全部流程页面看上去能翻页却很难称为可靠的做饭模式。本文基于“健康菜谱助手”1.0.5 的真实 HarmonyOS ArkTS 工程展开。项目在CookingStepsData.ets中为 60 道菜准备了 60 组步骤共 369 条CookingStepCookingStepsPage.ets已实现上一项、下一项、总览跳转和中文语音朗读。但当前步骤只保存在页面的State中离开后会回到第 1 步最后点击“完成”仅执行router.back()也没有把完成结果作为一个清晰事件交回业务层。烹饪步骤页的核心不是把 currentStep 加一而是让步骤位置、语音状态、完成语义和页面生命周期围绕同一个 CookingSession 演进。HEALTHY-RECIPE-ONE7-COOKING-SESSION-20260730本文会完成以下工作还原CookingStepsPage的真实路由、步骤与语音链路。用有限状态机约束上一步、下一步、跳转和完成。为返回恢复设计可持久化的CookingSession避免每次从头开始。把“进入做饭模式”和“完成做饭”拆成两个不同业务事件。解决切换步骤时语音仍在朗读上一条的问题。给出空步骤、非法 recipeId、重复点击和页面重建的验证方法。分析手机、平板和 2in1 下步骤页的布局边界。一、先盘点真实工程不从理想代码出发当前页面的核心状态非常精简State recipeName: string State steps: CookingStep[] [] State currentStep: number 0 State isSpeaking: boolean false State speechStatus: string 做饭模式可朗读当前步骤 StorageLink(appThemeColor) appThemeColor: string #4CAF50 private speechEngine?: textToSpeech.TextToSpeechEngine这段代码说明页面已经区分了步骤内容、当前位置和语音状态但还没有保存“本次烹饪会话”。currentStep的生命周期与组件实例相同组件销毁后它也消失。页面再次创建时字段初始化为0用户自然回到第一步。数据层的覆盖情况则相对完整。getCookingStepsById()对 recipeId 1 到 60 都有明确分支并保留STEPS_DEFAULT作为兜底export function getCookingStepsById(recipeId: number): CookingStep[] { switch (recipeId) { case 1: return STEPS_1 case 2: return STEPS_2 // ...一直覆盖到 60 case 60: return STEPS_60 default: return STEPS_DEFAULT } }统计真实源码可得到项目数量含义菜谱步骤数组60 组与 60 道菜的 recipeId 对应CookingStep记录369 条每道菜约 5 至 7 步页面当前位置1 个currentStep仅在当前组件内存有效完成回调0 个最后一步直接返回因此本篇不是补齐步骤文案而是把已有数据变成可恢复、可验证的操作流程。二、当前页面的运行链路页面出现时读取路由参数并据此加载菜名和步骤aboutToAppear(): void { const params router.getParams() as StepsRouterParams let recipeId 1 if (params params.recipeId) { recipeId params.recipeId } const recipe getRecipeById(recipeId) this.recipeName recipe ? recipe.name : 菜谱 this.steps getCookingStepsById(recipeId) }随后页面用三条交互路径修改currentStep点击“上一步”执行currentStep--。点击“下一步”执行currentStep。点击“全部步骤”中的任意行把currentStep直接设为对应索引。最后一项显示“完成”点击后释放语音引擎并返回if (this.currentStep this.steps.length - 1) { this.stopSpeech() this.currentStep } else { this.releaseSpeechEngine() router.back() }这条链路在一次连续操作中成立但把几个不同概念混在了一起currentStep既是展示索引也是进度。到达最后一步不等于已经完成点击完成才是业务确认。返回详情页可能是暂停也可能是放弃不能自动推断。页面消失会释放语音却不会保存步骤位置。非法路由被默认改成 recipeId 1用户可能在不知情时看到另一道菜。三、先定义 CookingSession而不是继续堆页面字段步骤页需要一个只描述会话的模型。它不复制菜谱文案只记录用户进行到哪里export type CookingSessionStatus idle | in_progress | paused | completed export interface CookingSession { recipeId: number currentStepIndex: number status: CookingSessionStatus startedAt: number updatedAt: number completedAt?: number }字段设计有几个有意的限制保存索引而不是整个CookingStep避免菜谱内容升级后持久化旧文案。保存recipeId用于与数据源重新关联。用status区分进行中、暂停和完成而不是用“是不是最后一步”猜测。updatedAt可用于判断是否继续上一次会话也能辅助排查异常恢复。completedAt只有用户确认完成后才存在。如果产品允许同一时间只做一道菜Preferences 保存一个活动会话即可如果要保留多道菜的进度可以按 recipeId 存成数组或使用 RDB。当前应用的用户数据规模很小使用 Preferences 足够但存储操作应放在服务层不能让页面直接拼 JSON。四、建立明确的步骤状态机“上一项”和“下一项”不是两个孤立按钮它们共同构成状态机。有效状态范围必须始终满足0 currentStepIndex steps.length可以先把边界计算封装成只读方法private hasPrevious(): boolean { return this.currentStep 0 } private hasNext(): boolean { return this.steps.length 0 this.currentStep this.steps.length - 1 } private isLastStep(): boolean { return this.steps.length 0 this.currentStep this.steps.length - 1 }然后只允许一个入口改变步骤private async moveToStep(target: number): Promisevoid { if (this.steps.length 0) { return } const safeIndex Math.max(0, Math.min(target, this.steps.length - 1)) if (safeIndex this.currentStep) { return } this.stopSpeech() this.currentStep safeIndex this.speechStatus 做饭模式可朗读当前步骤 await this.saveProgress(in_progress) }统一入口带来三个直接收益按钮、列表跳转和恢复逻辑共用同一边界校验。每次切换步骤都会停止旧语音避免声音与画面错位。保存进度的时机不再散落在多个onClick中。对应按钮只表达意图Button(上一步) .enabled(this.hasPrevious()) .onClick(() this.moveToStep(this.currentStep - 1)) Button(this.hasNext() ? 下一步 : 完成) .onClick(() { if (this.hasNext()) { this.moveToStep(this.currentStep 1) } else { this.confirmCompletion() } })五、路由参数必须先验证再读取步骤当前代码把缺少参数和非法参数都退回到 recipeId 1。这种兜底对演示友好对真实用户却不透明用户打开“清蒸鲈鱼”参数错误后可能看到“宫保鸡丁”的步骤。更合适的页面状态是loading | content | errortype StepPageState loading | content | error State pageState: StepPageState loading State errorMessage: string State recipeId: number 0初始化时执行严格校验private loadRoute(): void { const params router.getParams() as PartialStepsRouterParams const recipeId Number(params?.recipeId) const recipe Number.isInteger(recipeId) ? getRecipeById(recipeId) : undefined if (!recipe) { this.pageState error this.errorMessage 未找到对应菜谱请返回后重试 return } const steps getCookingStepsById(recipeId) if (steps.length 0) { this.pageState error this.errorMessage 这道菜暂时没有可用步骤 return } this.recipeId recipeId this.recipeName recipe.name this.steps steps this.restoreProgress() }这里不要依赖STEPS_DEFAULT掩盖数据问题。默认步骤适合开发占位但正式内容最好要求 60 个菜谱 ID 与 60 组步骤一一闭合缺失时显示明确错误并记录日志才能尽早发现数据回归。六、恢复进度时必须再次钳制索引持久化会话不是简单地把旧索引赋值回来。应用升级后某道菜可能从 7 步改成 5 步原来的索引 6 已越界。如果直接恢复this.steps[this.currentStep].title会访问undefined。可以把恢复规则写成独立函数function normalizeStepIndex(saved: number, stepCount: number): number { if (stepCount 0) { return 0 } if (!Number.isInteger(saved)) { return 0 } return Math.max(0, Math.min(saved, stepCount - 1)) }读取会话后验证 recipeId、状态和索引private async restoreProgress(): Promisevoid { const session await this.cookingSessionService.get(this.recipeId) if (!session || session.status completed) { this.currentStep 0 this.pageState content return } this.currentStep normalizeStepIndex( session.currentStepIndex, this.steps.length ) this.pageState content }完成会话默认从头开始是当前产品最容易理解的规则。如果产品希望用户回看最后一次完成位置可以保留完成记录但不要把它自动恢复成“进行中”。七、把保存结果变成可观察结果用户点到下一步时UI 可以先切换再异步保存但保存失败不能永久沉默。服务层应返回明确结果export interface SaveSessionResult { ok: boolean reason?: storage_unavailable | serialize_failed | write_failed } async save(session: CookingSession): PromiseSaveSessionResult { try { await this.preferences.put( cooking_session_${session.recipeId}, JSON.stringify(session) ) await this.preferences.flush() return { ok: true } } catch (_) { return { ok: false, reason: write_failed } } }页面不需要弹出阻断式对话框但应给出轻量提示并允许下一次操作重试private async saveProgress(status: CookingSessionStatus): Promisevoid { const result await this.cookingSessionService.save({ recipeId: this.recipeId, currentStepIndex: this.currentStep, status, startedAt: this.startedAt, updatedAt: Date.now() }) if (!result.ok) { promptAction.showToast({ message: 进度暂未保存请稍后重试 }) } }重点不在 Toast而在于页面不再把“内存已变更”误当成“持久化成功”。八、完成必须是独立业务事件当前详情页在进入做饭模式前就调用addCooked步骤页最后又只是返回。这样用户只要打开步骤就可能被统计为“已做”即使他在第一步退出。更准确的语义是用户动作会话状态是否写入已做记录打开步骤页in_progress否切换步骤in_progress否中途返回paused否点击最后一步“完成”completed是再次打开已完成菜谱新会话或回顾不重复写完成方法需要先保存再写业务记录最后返回private async confirmCompletion(): Promisevoid { if (!this.isLastStep()) { return } this.releaseSpeechEngine() const saved await this.cookingSessionService.complete({ recipeId: this.recipeId, currentStepIndex: this.currentStep, completedAt: Date.now() }) if (!saved.ok) { promptAction.showToast({ message: 完成状态保存失败请重试 }) return } UserDataManager.addCooked(this.cookedRecords, this.recipeId) router.back() }如果addCooked也可能失败最好由一个服务统一完成“会话完成 已做记录”并返回结果。Preferences 不提供数据库事务时可以设计幂等写入CookedRecord以 recipeId 为唯一键重复调用只更新时间不新增重复记录。九、语音朗读必须跟随步骤生命周期当前工程使用kit.CoreSpeechKit的textToSpeech按在线模式 1、0 依次尝试创建引擎const engineModes: number[] [1, 0] for (const mode of engineModes) { try { return await textToSpeech.createEngine({ language: zh-CN, person: 0, online: mode }) } catch (_) { } }这个降级思路是合理的先尝试一种工作模式失败后再切换。页面也在aboutToDisappear()中调用releaseSpeechEngine()避免引擎随页面泄漏。不过还要注意三个细节。9.1 切换步骤前停止旧请求项目现有的按钮和步骤列表已经调用stopSpeech()这是正确方向。统一到moveToStep()后可以避免未来新增手势时漏掉。9.2 不用布尔值推断请求身份isSpeaking只能说明“有声音”不能说明回调属于哪一步。快速点击两次时旧请求的onComplete可能晚于新请求到达导致状态被旧回调覆盖。可以保存当前 requestIdprivate activeSpeechRequestId: string private createSpeechRequestId(): string { return recipe_${this.recipeId}_step_${this.currentStep}_${Date.now()} }监听回调时只处理当前请求onComplete: (requestId: string) { if (requestId ! this.activeSpeechRequestId) { return } this.isSpeaking false this.speechStatus 本步骤朗读完成 }具体回调签名应以项目实际 SDK 版本的官方 API 为准核心原则是让异步回调带着身份回来不能只依靠全局布尔值。9.3 空文本不发起朗读当前buildCurrentStepSpeechText()在步骤不存在时返回空字符串。调用前再判断一次能让错误更容易定位const text this.buildCurrentStepSpeechText().trim() if (!text) { this.speechStatus 当前步骤没有可朗读内容 return }十、列表跳转后还要考虑滚动位置当前页面把“当前步骤详情”和“全部步骤”放在同一个Scroll中。用户在底部总览点到第 2 步后currentStep虽然改变滚动位置仍可能停在列表底部新的步骤标题没有立刻出现在视口中。可以使用Scroller管理内容位置private contentScroller: Scroller new Scroller() private async moveToStep(target: number): Promisevoid { // 校验、停语音、保存逻辑略 this.currentStep safeIndex this.contentScroller.scrollTo({ xOffset: 0, yOffset: 0, animation: { duration: 220, curve: Curve.EaseOut } }) }在做饭场景中“点了以后马上看到当前步骤”比保留总览列表位置更符合预期。若平板采用左右分栏总览列表可以拥有独立滚动容器就不需要每次回到顶部。十一、页面应该显式渲染四种状态当前if (steps.length 0)只控制内容区空数据时底部仍可能出现1 / 0和“完成”。更完整的 UI 分支是if (this.pageState loading) { LoadingProgress() } else if (this.pageState error) { StepErrorView({ message: this.errorMessage, onBack: () router.back() }) } else if (this.steps.length 0) { EmptyStepView({ onBack: () router.back() }) } else { this.StepContent() this.StepActions() }四种状态对应四类测试加载过程不会误触按钮错误状态能返回空数据不会显示假进度内容状态才允许步骤操作。十二、响应式布局不能只把手机页面拉宽工程声明支持 phone、tablet 和 2in1。步骤页在手机上适合纵向流程但平板和 PC/2in1 如果仍把 220vp 图片拉满整行会产生过宽图片和很长的视线移动。推荐按窗口宽度切换结构宽度区间布局操作区小于 600vp当前步骤在上、全部步骤在下底部固定上一项/下一项600–960vp左侧当前步骤、右侧步骤总览操作固定在左侧内容底部大于 960vp中央内容限宽右侧会话信息支持鼠标与键盘焦点ArkUI 中可以根据窗口分类计算布局而不是让页面组件直接读取大量设备信息type StepLayoutMode compact | medium | expanded function resolveStepLayout(widthVp: number): StepLayoutMode { if (widthVp 600) return compact if (widthVp 960) return medium return expanded }底部操作区还要保留系统导航安全距离。手机手势导航下按钮区域至少使用系统避让区与 28vp 中较大值长列表必须能滚动到最后一项不能被底部栏永久盖住。十三、状态层、服务层和页面层如何分工建议把职责拆成四层13.1 数据层CookingStepsData.ets继续保存发布随包的菜谱步骤。它负责 recipeId 到只读步骤数组的映射不保存用户进度。13.2 会话服务CookingSessionService负责加载、保存、暂停、完成和清理会话。页面只接收CookingSession或明确错误。13.3 语音适配器CookingSpeechService封装 SpeechKit 引擎创建、请求 ID、停止和释放。这样页面测试不依赖真实语音服务也能模拟创建失败、朗读失败和完成回调。13.4 页面页面持有渲染状态响应点击并调用服务。它不直接拼 Preferences key也不决定CookedRecord的去重规则。一个简化接口可以是export interface CookingSessionService { restore(recipeId: number, stepCount: number): PromiseCookingSession move(recipeId: number, stepIndex: number): PromiseSaveSessionResult pause(recipeId: number, stepIndex: number): PromiseSaveSessionResult complete(recipeId: number, stepIndex: number): PromiseSaveSessionResult }接口围绕用户动作命名比暴露put()、get()更能守住业务语义。十四、返回动作要区分系统返回和完成用户点击顶部返回或系统返回键时应把当前会话保存为paused点击“完成”才保存为completed。两条路径不能都只执行router.back()。private async pauseAndBack(): Promisevoid { this.releaseSpeechEngine() await this.saveProgress(paused) router.back() }如果系统返回拦截能力在当前 API 版本中可用应让它调用同一方法如果没有拦截就在页面生命周期中做幂等暂停保存。需要防止“完成后页面消失”又把状态覆盖回 pausedprivate completionCommitted: boolean false aboutToDisappear(): void { this.releaseSpeechEngine() if (!this.completionCommitted this.pageState content) { this.saveProgress(paused) } }completionCommitted是短生命周期保护不是持久化真源。真正状态仍以服务层保存结果为准。十五、重复点击和异步竞态怎么处理做饭时手上有水用户可能连续点击“下一步”。如果每次点击都立即启动异步保存旧请求可能最后完成并把较小索引写回存储。最简单的方案是页面保存期间禁用步骤切换State isSavingProgress: boolean false private async moveToStep(target: number): Promisevoid { if (this.isSavingProgress) return this.isSavingProgress true try { // 计算 safeIndex、更新 UI、持久化 } finally { this.isSavingProgress false } }更流畅的方案是给每次保存附加递增版本号服务层只接受比已保存版本新的会话。对当前规模而言短暂禁用按钮已经足够清晰也便于测试。十六、从真实数据中建立内容完整性检查CookingStepsData.ets目前包含 60 组、369 条步骤。发布前可以写一个只读检查确保每组步骤至少包含标题和描述且 stepNumber 连续interface StepValidationIssue { recipeId: number message: string } function validateSteps(recipeId: number, steps: CookingStep[]): StepValidationIssue[] { const issues: StepValidationIssue[] [] if (steps.length 0) { issues.push({ recipeId, message: 步骤为空 }) return issues } steps.forEach((step, index) { if (step.stepNumber ! index 1) { issues.push({ recipeId, message: 第 ${index 1} 项编号不连续 }) } if (!step.title.trim() || !step.description.trim()) { issues.push({ recipeId, message: 第 ${index 1} 项缺少标题或描述 }) } }) return issues }这个检查不替代厨艺内容审核但可以阻止空标题、编号跳跃和缺失说明进入页面。十七、建议的回归测试矩阵编号场景操作预期S01正常进入从详情页打开 recipeId 1显示宫保鸡丁步骤进度为 1/NS02非法参数recipeId 为字符串或不存在显示错误态不静默换菜S03上一步边界在第 1 步点击上一步按钮禁用索引仍为 0S04下一步边界连续前进到最后一项按钮文案变成完成不越界S05总览跳转从第 1 步点第 5 步停止旧语音内容定位到第 5 步S06返回恢复第 4 步返回再打开恢复第 4 步并显示 paused 会话S07数据升级保存索引大于新步骤数钳制到最后一个有效索引S08完成语义最后一步点击完成写入 completed 和已做记录S09中途退出第 2 步系统返回只保存 paused不记已做S10语音切步朗读时点下一步旧朗读停止新页面状态正确S11引擎失败模拟 SpeechKit 创建失败页面仍可操作提示语音不可用S12保存失败模拟 Preferences 写入失败显示轻提示不谎报已保存S13重复点击快速点击下一步 5 次不越界不发生旧进度覆盖S14空步骤数据层返回空数组显示空态底部操作栏隐藏S15页面销毁朗读时离开页面引擎停止并 shutdown无后台声音十八、可复核的验收步骤18.1 静态数据检查遍历 recipeId 1 到 60。确认每个 ID 都返回非空步骤。确认每组stepNumber从 1 连续递增。确认标题、描述和图片资源引用不为空。记录总步骤数当前基线应为 369。18.2 状态恢复检查打开任一道至少 6 步的菜谱。前进到第 4 步并返回详情页。再次进入步骤页确认恢复到第 4 步。完成后重新进入确认按产品规则从第 1 步开始新会话。清除本地数据后进入确认不会读取旧索引。18.3 语音生命周期检查点击朗读确认状态从准备变为朗读中。朗读中点击步骤总览另一项确认旧声音立即停止。朗读中返回确认离开页面后没有残留声音。关闭系统语音能力或模拟创建失败确认步骤按钮仍可使用。快速重复点击朗读确认不会并发播放两条内容。18.4 多设备检查手机竖屏检查底部安全区和长描述滚动手机横屏检查标题与按钮不挤压平板检查图片不被无上限拉宽2in1 检查鼠标点击、焦点态和窗口缩放。系统深色模式下还要验证正文、禁用按钮、进度条和底栏阴影的对比度。十九、常见问题与定位方法19.1 返回后总是第 1 步原因currentStep只存在于State页面重建后初始化为 0。处理用会话服务保存 recipeId、索引和状态页面加载完成后恢复并钳制索引。19.2 点击完成但“已做”统计不准确原因进入步骤页时就写了已做记录完成按钮没有独立业务回调。处理进入只创建in_progress会话最后确认后才写completed与已做记录。19.3 画面已切到下一步声音还在读上一条原因新的步骤入口没有统一停止当前语音或旧请求回调覆盖新状态。处理所有步骤变化都走moveToStep()并用 requestId 识别回调身份。19.4 空步骤时出现“1 / 0”原因内容区判断了steps.length 0底部操作栏没有使用同一条件。处理让内容和操作栏共同属于 content 状态空数据时只渲染空态。19.5 恢复后页面崩溃原因保存索引大于升级后的步骤总数。处理每次恢复都通过normalizeStepIndex()钳制不信任持久化值。19.6 快速点击后恢复到了更早一步原因多次异步保存乱序完成较旧请求最后写入。处理保存期间禁用交互或引入单调递增版本号并拒绝旧写入。二十、工程落地顺序不要一次性同时改页面、语音和所有数据。更稳的顺序是先增加路由参数校验与 loading/error/empty/content 状态。再把所有步骤变化收口到moveToStep()。新增CookingSession与服务层完成保存和恢复。调整完成语义删除“进入即已做”的写入时机。最后封装语音请求身份和多设备布局。每一步都重跑 S01–S15 的相关用例。这样每次修改都有单一目标先不越界再能恢复再保证完成语义最后提升语音与布局体验。二十一、源码事实与建议实现的边界为了避免把设计方案误写成项目已经具备的能力下面把本文结论按真实源码快照与建议实现重新归档。读者复核时可以直接从CookingStepsPage.ets、CookingStepsData.ets、RecipeDetailPage.ets和UserDataManager.ets对照。检查项当前源码事实本文建议不代表已经落地步骤数据getCookingStepsById()对 recipeId 1 至 60 有明确分支共 60 个步骤数组、369 条CookingStep非法 ID 返回STEPS_DEFAULT为正式数据增加构建期完整性检查缺失时进入明确错误态路由处理参数缺失时默认 recipeId 1参数存在但查不到菜谱时标题显示“菜谱”步骤仍可能使用默认数组严格校验整数 ID非法参数显示错误并允许返回当前位置currentStep是页面State初始值为 0页面重建后没有恢复逻辑引入CookingSession保存 recipeId、索引、状态和时间步骤切换上一步、下一步和总览点击分别直接修改currentStep修改前会调用stopSpeech()收口到moveToStep()统一边界校验、滚动定位和保存完成动作最后一步点击“完成”只执行releaseSpeechEngine()与router.back()把完成定义为独立事件成功写入 completed 后再记录已做已做记录RecipeDetailPage在进入步骤页前调用UserDataManager.addCooked()因此打开后立即返回也可能被统计为已做将写入时机移动到最后一步确认完成并保持 recipeId 幂等返回语义顶部返回只执行router.back()aboutToDisappear()只释放语音资源返回保存 paused完成保存 completed防止生命周期回调覆盖完成态语音模式当前引擎按online: 1、online: 0依次尝试页面离开时 stop 并 shutdown用适配器封装请求身份、失败原因和测试替身网络与隐私说明按真实模式保持一致空数据渲染内容区有steps.length 0判断但底部操作栏始终渲染理论上可能出现“1 / 0”页面显式区分 loading、error、empty、content空态隐藏操作栏多设备与构建源码可证明页面使用百分比宽度、滚动区和layoutWeight本文没有执行设备安装或发布构建在 phone、tablet、2in1 上验证安全区、宽屏分栏、键鼠焦点和窗口缩放这张表也限定了测试结论本文给出的状态机、会话服务和回归矩阵是可执行的改造方案不是对当前安装包行为的虚构描述。只有在代码落地、构建通过并完成设备验证后才能把相应建议改写成“已实现”。二十二、总结健康菜谱助手已经具备 60 组、369 条真实步骤以及上一步、下一步、总览跳转和 SpeechKit 朗读能力。真正影响可靠性的不是页面少一个按钮而是步骤进度没有会话边界返回后无法恢复“完成”也没有成为独立业务事件。把页面改造成稳定做饭模式需要坚持四条原则recipeId 必须校验不能用另一道菜静默兜底。currentStep 必须受状态机约束并在恢复时重新钳制。paused 与 completed 必须分开只有确认完成才记录已做。语音请求必须跟随步骤和页面生命周期切步、返回时及时停止。当步骤位置、保存结果、语音状态和完成记录都能被明确观察和验证后这个页面才从“可以翻步骤”升级为“用户真的能在厨房里放心使用”。参考资料HarmonyOS ArkUI 概述HarmonyOS ArkTS 概述HarmonyOS 数据管理概述HarmonyOS AppStorage 开发指导工程源码CookingStepsPage.ets、CookingStepsData.ets、RecipeModel.ets、RecipeDetailPage.etsAI 辅助声明本文由 AI 辅助完成源码统计、结构校对与配图设计所有当前能力判断均以文中列出的本地 ArkTS 源码为依据并由作者复核后发布建议代码和验证矩阵不代表工程已经构建或上线。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑