App7 水滴打卡站:每日科学饮水计划的HarmonyOS开发实践

摘要

「水滴打卡站」是 AI40 智能应用工具箱中编号为 7 的健康生活类应用,核心功能是根据用户输入的体重和选择的活动量等级,智能生成一份个性化的每日科学饮水计划,包含每日目标饮水量、分时段饮水时间表以及科学饮水建议。本文从 6A 工作流(Align-Architect-Atomize-Approve-Automate-Assess)的视角,系统性地记录了该应用从需求分析到 ArkTS 代码实现的完整过程,深入分析了 ArkTS 严格模式下的数据接口设计、状态管理策略、场景化 Mock 数据生成逻辑、UI 组件树设计以及编译合规性审计,为 HarmonyOS 开发者提供了一套可复用的健康类 AI 应用开发范式。

在 AI40 工具箱的 40 个应用中,「水滴打卡站」作为健康生活赛道的代表应用,与「晨光能量站」(App11,早餐推荐)和「心流计时器」(App4,番茄工作法)共同构成了日常健康管理的基础工具链。该应用的独特之处在于其基于科学饮水公式(体重 x 活动系数)的 Mock 数据生成策略,以及预留的 AI API 调用接口,为未来接入鸿蒙原生 AI 大模型提供了平滑的升级路径。

在这里插入图片描述
在这里插入图片描述

一、Align 对齐阶段:从需求到规范

1.1 应用背景与原始需求分析

「水滴打卡站」的原始需求来源于一个明确的用户痛点:绝大多数人每天的饮水量不足,而且缺乏科学、可执行的饮水计划。根据《中国居民膳食指南(2022)》的建议,成年男性每日饮水量约为 1700ml,成年女性约为 1500ml,但实际摄入量往往远低于此。更关键的是,"一次性喝够"远不如"少量多次"的饮水方式健康——肾脏对水分的处理能力有限,单次大量饮水不仅无法被有效吸收,还可能加重肾脏负担。

基于这一痛点,「水滴打卡站」需要解决以下核心问题:

  1. 个性化饮水目标计算:不同体重和活动量的人,每日所需水分差异巨大。一个 50kg 的久坐办公族和一个 80kg 的健身爱好者,饮水需求可能相差一倍以上。
  2. 分时段饮水计划:将每日总饮水量合理分配到一天中的各个时段,形成可执行的"打卡"时间表,帮助用户养成定时饮水的习惯。
  3. 科学饮水建议:每个时段附带具体的饮水提示,告诉用户"为什么现在要喝水"以及"怎么喝对身体最好"。

原始 Prompt 模板定义了三个输入参数和对应的输出格式:

  • 输入参数:体重(kg,数值输入)、活动量(枚举:低/中/高)
  • 输出格式:每日目标饮水量(goal_ml)、分时段饮水时间表(schedule,包含时间点、饮水量、饮用提示)、综合饮水建议(tips)
  • 兜底规则:当用户未输入体重时,默认按 65kg 中等活动量生成计划

1.2 功能边界确认

基于原始需求,我们在对齐阶段明确了「水滴打卡站」的功能边界:

核心功能(In Scope):

  • 体重输入:支持用户通过 TextInput 输入体重值(kg),使用数字键盘,默认值为 65kg
  • 活动量选择:提供低、中、高三个活动量等级的下拉选择器,默认为"中"
  • 饮水计划生成:基于体重和活动量计算出每日目标饮水量,并生成分时段饮水时间表
  • 结果展示:展示每日目标、饮水时间表(含时间、饮水量、提示)、综合饮水建议
  • Loading 状态:生成过程中展示加载动画,模拟 AI 计算过程

不在范围内(Out of Scope):

  • 实际饮水打卡与记录功能:本应用仅生成计划,不包含每日打卡、饮水记录、历史统计等功能
  • 推拉通知提醒:不包含定时推送饮水提醒功能(需要通知权限和后台任务)
  • 饮水数据持久化:不将用户的饮水计划存储到本地数据库或云端
  • 多用户管理:仅支持单用户场景,不支持家庭成员管理
  • 与健康 App 数据同步:不接入 HarmonyOS 健康数据框架

交互流程: 参数输入(体重 + 活动量)→ 点击"AI 生成"按钮 → 显示 Loading 动画(800ms 模拟延迟)→ 展示结果。这是一个经典的三段式"输入 → 处理 → 输出"交互模式,用户操作路径清晰明了,无需额外的引导步骤。

1.3 技术约束确认

在 ArkTS 严格模式下,「水滴打卡站」的开发需要特别关注以下技术约束。这些约束并非可选的最佳实践,而是违反后将导致编译失败的硬性要求:

A. 类型系统约束

  • 禁止 any 和 unknown 类型:所有变量、参数、返回值必须使用显式类型标注。这意味着饮水计划的数据结构必须通过 explicit interface 定义——DrinkSchedule(单条饮水时段)和 DrinkOutput(完整饮水计划输出)两个接口。
  • 禁止解构赋值:在 generateMockData() 方法中,不能使用 const { weight, activity } = this 这样的解构语法,必须通过 const weight: number = parseFloat(this.weightKg); const activity: string = this.selectedActivity; 逐字段赋值。
  • 禁止索引访问类型:不能使用 obj[key] 的索引访问方式,必须使用 obj.field 的点访问方式。
  • 禁止条件类型别名和 infer 关键字:所有类型关系必须通过显式继承或类型别名来定义。

B. 语法约束

  • 禁止 for…in 循环:饮水时间表的遍历必须使用 ForEach 组件或常规 for 循环,不能使用 for..in 遍历对象属性。
  • 禁止 var 关键字:所有变量声明必须使用 let 关键字。
  • 禁止嵌套函数:所有嵌套函数必须改为箭头函数(lambda)形式。例如,onClick 回调中使用 (): void => { ... } 而非 function 声明。
  • 禁止函数表达式:必须使用箭头函数来显式指定函数类型。
  • 禁止在独立函数和静态方法中使用 this:this 只能在实例方法中使用。

C. 组件约束

  • @State 属性名冲突风险:ArkUI 框架内置了大量属性名(如 width、height、color、padding 等),如果 @State 变量与这些内置属性名冲突,会导致不可预期的行为。本应用中使用了 weightKg(而非 weight)和 selectedActivity(而非 activity)来避免潜在冲突。
  • Scroll 组件单子组件约束:Scroll 组件只能包含一个直接子组件,所有可滚动内容必须包裹在一个 Column 中。这是 ArkUI 组件树在 鸿蒙PC 端和大屏设备上保持一致渲染行为的重要约束。
  • 不支持 JSX 表达式:所有 UI 必须使用 ArkUI 的声明式语法构建。

D. API 约束

  • 不支持 Function.apply / Function.call / Function.bind:必须遵循传统的 OOP 风格来处理 this 的语义。
  • 不支持 Symbol() API(除 Symbol.iterator 外):不能使用 Symbol 作为对象键。
  • 不支持 globalThis 和全局作用域:所有数据共享必须通过显式的模块导出和导入。

1.4 验收标准

以下验收标准在对齐阶段制定,并在审批阶段逐一验证:

维度 标准 验证方式 优先级
参数覆盖 体重 1 种输入 + 活动量 3 种枚举 = 3 种场景组合 低/中/高三种活动量均生成不同计划 P0
结果完整性 每次生成包含 goal_ml、schedule(时间表)、tips(建议)三个部分 结果展示页面验证 P0
饮水公式准确性 中活动量 = 体重 × 30ml,高活动量 = 体重 × 40ml,低活动量 = 体重 × 25ml 手动计算验证 P0
目标值取整 目标饮水量按 100ml 为单位取整(Math.round(baseMl / 100) * 100) 边界值测试 P1
离线可用 无网络环境下正常生成结果(使用 Mock 数据) 飞行模式测试 P0
编译合规 零 Error,通过 ArkTS 严格模式编译 DevEco Studio 编译 P0
UI 交互 活动量下拉选择器可正常展开/收起/选中,选中项高亮显示 手动操作验证 P1
Loading 状态 生成过程中展示 LoadingProgress 组件和"AI正在生成饮水计划…"提示文字 视觉验证 P1
空状态 未生成结果时展示"请输入体重和活动量后点击生成"引导文字 视觉验证 P2
返回导航 点击"← 返回"按钮可正常返回上一页 路由跳转验证 P1

二、Architect 架构阶段:数据结构与组件设计

2.1 数据接口设计

「水滴打卡站」的数据模型分为两层:单条饮水时段(DrinkSchedule)和完整饮水计划输出(DrinkOutput)。这两个接口的设计遵循了 ArkTS 严格模式的全部类型约束,所有字段均使用显式类型标注。

// 单条饮水时段:描述一天中某个时间点的饮水安排
interface DrinkSchedule {
  time: string;    // 饮水时间,格式 "HH:MM",如 "07:00"
  ml: string;      // 建议饮水量,格式 "XXXml",如 "300ml"
  tip: string;     // 饮水提示,解释该时段饮水的原因和注意事项
}

// 完整饮水计划输出:包含每日目标、时间表和综合建议
interface DrinkOutput {
  goal_ml: string;           // 每日目标饮水量,如 "2000ml"
  schedule: DrinkSchedule[]; // 分时段饮水时间表,数组长度 6-9 条
  tips: string;              // 综合饮水建议,包含科学饮水原则
}

这个接口设计有几个值得深入分析的细节:

时间与饮水量的字符串表示: 我们将 timeml 都设计为 string 类型而非 number 类型。time 使用 “HH:MM” 格式字符串而非分钟数,是因为在 UI 中直接展示 “07:00” 比展示 “420”(分钟数)更直观。ml 使用 “300ml” 格式字符串而非纯数字,同样是为了在 UI 中直接渲染,减少格式化逻辑。这种"展示层优先"的数据设计策略在简单应用中非常有效——它避免了在 UI 层进行额外的数据转换,降低了代码复杂度。但它的代价是放弃了数据计算能力(如对饮水量求和),如果未来需要统计数据,需要在另一个维度维护数值型数据。

嵌套数组 vs 扁平结构: 我们将 schedule 设计为 DrinkOutput 的嵌套数组,而非单独的顶层状态。这种设计的好处是:schedule 与 goal_ml 和 tips 是强关联的——它们共同构成一份完整的饮水计划,放在同一个数据结构中便于整体管理。在 Mock 数据生成时,整个 DrinkOutput 对象一次性赋值给 this.outputData,避免了多次状态更新导致的重复渲染。在鸿蒙的 鸿蒙Flutter框架 中,类似的数据嵌套也是推荐的做法,因为它能减少 Widget 树的重建次数。

snake_case 字段命名: 我们使用了 goal_ml 而非 goalMl 作为字段名。这并非 ArkTS 的强制要求,而是考虑到未来接入 AI API 时,大多数 AI 模型输出的 JSON 通常使用 snake_case 命名约定。提前在接口层对齐命名风格,可以避免未来在 JSON 解析时进行字段映射。不过,这也意味着在 ArkTS 代码中访问字段时需要使用 this.outputData.goal_ml 而非 this.outputData.goalMl,略微降低了代码的可读性。

可空类型的处理: outputData 状态的类型标注为 DrinkOutput | null,而非 DrinkOutput | undefined。这是因为 ArkTS 不支持 delete 属性操作——对象的布局在编译时已知且运行时不可更改。使用 null 作为"无数据"的标记,符合 ArkTS 的设计哲学:声明一个可空类型并赋值为 null 来标记值的缺失,而不是尝试删除属性。

2.2 状态管理设计

「水滴打卡站」使用了 ArkUI 的 @State 装饰器进行状态管理,共定义了 6 个状态变量:

@State weightKg: string = '65';              // 体重输入值(字符串,便于 TextInput 绑定)
@State selectedActivity: string = '中';      // 当前选中的活动量等级
@State outputData: DrinkOutput | null = null; // 生成的饮水计划结果
@State isLoading: boolean = false;            // 加载状态标识
@State hasResult: boolean = false;            // 是否有结果可展示

此外,还有两个非响应式变量:

private activityOptions: string[] = ['低', '中', '高'];  // 活动量选项列表(静态数据)
private showActivityPicker: boolean = false;               // 下拉选择器展开/收起状态

状态管理的设计决策分析:

为什么 weightKg 使用 string 而非 number? TextInput 组件的 text 属性和 onChange 回调都使用字符串类型。如果 weightKg 使用 number 类型,每次 onChange 回调都需要进行字符串到数字的转换,增加了类型转换的复杂度。在 generateMockData() 中需要计算时,再通过 parseFloat(this.weightKg) 进行一次性转换,这是一个合理的权衡。不过这也引入了一个潜在问题:如果用户输入了非数字字符(如 “65a”),parseFloat 会返回 NaN,需要在计算逻辑中进行防御。

为什么 hasResult 和 outputData 同时存在? 从逻辑上讲,hasResult 可以通过 this.outputData !== null 来推断,似乎是一个冗余状态。但在实际代码中,hasResulttrueoutputDatanull 是不应该出现的情况,反之亦然。保持两个独立的状态变量虽然增加了少量的状态同步负担,但让 UI 条件渲染逻辑更加清晰——模板中可以直接使用 if (this.hasResult && this.outputData !== null) 同时检查两个条件,提供了双重安全保障。

为什么 showActivityPicker 不是 @State? 下拉选择器的展开/收起状态使用 private 修饰符而非 @State,这是因为 showActivityPicker 不直接参与 UI 渲染——它只是控制一个条件性 if 块的显示/隐藏。实际上,这个设计存在一个微妙的问题:在 ArkUI 中,非 @State 变量的变化不会触发 UI 重新渲染。但在这个特定场景中,onClick 回调修改了 showActivityPicker,而同一个 onClick 通常也会触发 selectedActivity 的修改(@State),后者会触发整个组件的重新渲染,从而间接使得 showActivityPicker 的变更生效。如果这种隐式依赖在未来被打破(例如,仅切换展开/收起而不修改选中项),下拉选择器将无法正常响应。这是一个值得注意的设计隐患。

2.3 UI 组件树设计

「水滴打卡站」的 UI 组件树采用经典的纵向布局,整体结构如下:

Column (根容器, 背景色 #F5F5F5)
├── Row (顶部导航栏, 背景色 #FFFFFF)
│   ├── Text ("← 返回", 点击触发 router.back())
│   ├── Text ("水滴打卡站", 标题居中, layoutWeight)
│   └── Text (占位, width 60px, 保持标题居中)
│
└── Scroll (可滚动区域, layoutWeight)
    └── Column (唯一子组件, padding 16px)
        ├── Text ("体重(kg)", 输入标签)
        ├── TextInput (体重输入, 数字键盘, 绑定 weightKg)
        ├── Text ("活动量", 输入标签)
        ├── Row (活动量选择器触发区, 点击展开/收起)
        │   ├── Text (当前选中值)
        │   └── Text ("▼", 下拉箭头)
        ├── [条件] Column (下拉选项列表, showActivityPicker 控制)
        │   └── ForEach → Row (单个选项, 选中高亮)
        ├── Row (按钮容器, 居中)
        │   └── Button ("AI 生成", 主题色 #4ECDC4)
        ├── [条件] Column (Loading 状态, isLoading 控制)
        │   ├── LoadingProgress (旋转动画)
        │   └── Text ("AI正在生成饮水计划...")
        ├── [条件] Column (结果展示, hasResult 控制)
        │   ├── Text ("饮水计划", 结果标题)
        │   ├── Row (每日目标卡片, 背景色 #F0FDF9)
        │   │   ├── Text ("每日目标", 标签)
        │   │   └── Text (goal_ml, 主题色大字体)
        │   ├── Text ("饮水时间表", 子标题)
        │   ├── ForEach → Row (单个饮水时段卡片, 白底边框)
        │   │   ├── Column (时间和饮水量, 左侧 60px)
        │   │   │   ├── Text (time, 粗体)
        │   │   │   └── Text (ml, 主题色)
        │   │   └── Text (tip, 提示文字, layoutWeight)
        │   ├── Text ("饮水建议", 子标题)
        │   └── Text (tips, 黄色背景 #FFF9E6)
        └── [条件] Text (空状态引导, !isLoading 且无结果)

组件树的设计遵循了几个关键原则:

  1. Scroll 单子组件约束:所有可滚动内容(输入区 + 结果区)包裹在一个 Column 中,作为 Scroll 的唯一直接子组件。这个约束在 鸿蒙PC 端尤为重要——PC 端窗口尺寸可变,内容可能超出视口,Scroll 组件确保内容在任何窗口尺寸下都可滚动浏览。

  2. 条件渲染三段式:通过 isLoadinghasResult!isLoading && !hasResult 三个条件分支,实现了加载态、结果态、空态三种 UI 状态的互斥展示。这种三段式条件渲染是 AI40 工具箱中所有应用的通用模式,确保了用户体验的一致性。

  3. 导航栏等宽占位:使用了一个空的 Text('').width(60) 来平衡左侧的返回按钮(约 60px 宽),确保标题文字在视觉上居中。这是一种常见的居中技巧,在无法使用绝对定位的声明式 UI 框架中特别实用。

  4. ForEach 组件遍历:饮水时间表使用 ForEach 组件遍历 schedule 数组,每个时段渲染为一个独立的 Row 卡片。ForEach 是 ArkUI 中用于列表渲染的核心组件,它比传统的 for 循环更高效,因为框架可以追踪每个列表项的身份并进行增量更新。

2.4 数据流设计

「水滴打卡站」的数据流是单向的,遵循"用户输入 → 状态更新 → 计算生成 → 结果渲染"的经典模式:

用户操作               状态变化                    计算逻辑              UI更新
───────               ────────                    ────────              ──────
输入体重    ──────→  weightKg 更新                      │
选择活动量  ──────→  selectedActivity 更新              │
点击生成    ──────→  isLoading = true           ──────→  Loading UI
                    hasResult = false
                         │
                    setTimeout 800ms
                         │
                         ↓
                    generateMockData()  ──────→  outputData 更新
                         │
                         ↓
                    isLoading = false         ──────→  结果 UI
                    hasResult = true

这种单向数据流有几个优点:

  • 状态可追溯:任何时候 UI 的状态都可以通过 6 个 @State 变量完整描述,便于调试和测试。
  • 无副作用:generateMockData() 方法仅依赖 weightKg 和 selectedActivity 的值,不依赖外部状态,也不修改除 outputData 之外的状态,符合纯函数的设计理念。
  • 易于扩展:未来接入 AI API 时,只需将 generateMockData() 替换为 callAIAPI(),数据流路径不变。

2.5 异常处理策略

虽然「水滴打卡站」的当前实现中没有显式的异常处理代码(因为 Mock 数据生成逻辑不太可能出错),但在架构阶段我们规划了以下异常处理策略:

异常场景 处理策略 实现方式
体重输入为空 使用默认值 65kg aboutToAppear 中初始化
体重输入非数字 parseFloat 返回 NaN 后续版本需添加 isNaN 检查
体重输入为 0 或负数 生成无意义的饮水计划 后续版本需添加最小值校验
AI API 调用超时 超时后降级到 Mock 数据 callAIAPI 中添加 try-catch 和超时机制
AI API 返回格式错误 降级到 Mock 数据 + 提示用户 JSON 解析异常时 catch 并 fallback

三、Atomize 原子化阶段:任务分解与深度解析

3.1 原子任务分解

在对齐阶段和技术架构确认后,我们将「水滴打卡站」的开发工作分解为以下原子任务。每个原子任务都是独立可完成、可验证的最小工作单元:

任务编号 任务名称 输入 输出 预估工时 依赖
T1 定义数据接口 需求文档 DrinkSchedule、DrinkOutput 接口定义 15min
T2 实现体重输入组件 接口定义 TextInput 体重输入 + 标签 20min T1
T3 实现活动量选择器 接口定义 下拉选择器(低/中/高) 30min T1
T4 实现 Mock 数据生成逻辑 接口定义 + 饮水公式 generateMockData() 方法 40min T1
T5 实现 Loading 状态 接口定义 LoadingProgress + 提示文字 15min T1
T6 实现结果展示 UI 接口定义 目标卡片 + 时间表 + 建议 45min T1, T4
T7 实现顶部导航栏 路由依赖 返回按钮 + 标题 15min
T8 实现页面整体布局 T2-T7 完成 Scroll + Column 布局组合 25min T2-T7
T9 预留 AI API 调用接口 接口定义 注释的 callAIAPI() 方法 10min T1
T10 ArkTS 编译合规性检查 完整代码 零 Error 编译通过 20min T8
T11 视觉验收测试 完整代码 三种活动量场景 + 空状态 + Loading 30min T10

3.2 Mock 数据设计策略深度解析

Mock 数据是「水滴打卡站」最核心的技术实现,也是 AI40 工具箱中所有应用统一采用的离线优先策略。在真实的 AI API 接入之前,Mock 数据承担了"模拟 AI 输出"的角色,其设计质量直接决定了用户体验的真实感和产品的可用性。

3.2.1 饮水公式的科学依据

Mock 数据生成的核心是饮水公式,它基于医学和营养学领域的通用建议:

基础饮水量 = 体重(kg) × 活动系数

活动系数映射:
  - 低活动量:系数 25ml/kg(久坐办公、日常通勤)
  - 中活动量:系数 30ml/kg(轻度运动、日常活动)
  - 高活动量:系数 40ml/kg(健身训练、体力劳动)

以默认体重 65kg 为例:

  • 低活动量:65 × 25 = 1625ml,取整后为 1600ml
  • 中活动量:65 × 30 = 1950ml,取整后为 2000ml
  • 高活动量:65 × 40 = 2600ml,取整后为 2600ml

这个公式的设计参考了《中国居民膳食指南(2022)》中"每公斤体重约需 30-40ml 水"的建议,并结合了运动医学中"运动后需额外补充 500-800ml"的指导原则。取整逻辑(Math.round(baseMl / 100) * 100)将目标值对齐到 100ml 的整数倍,既便于用户记忆,也符合饮水容器的常见规格(200ml 水杯、300ml 马克杯、500ml 矿泉水瓶)。

3.2.2 场景化时间表设计

不同于简单的"总饮水量 ÷ 时段数"的均匀分配,「水滴打卡站」的饮水时间表是场景化的——每个时段的水量和提示都与人体的生物节律和日常活动节奏相匹配。以下是三种活动量等级的时间表设计逻辑:

中活动量场景(8 个时段,目标 2000ml):

时间 饮水量 设计逻辑
07:00 300ml 起床后第一杯温水,经过一夜睡眠身体处于脱水状态,需要及时补水唤醒代谢
09:00 250ml 早餐后补水,此时身体开始进入工作模式,水分帮助营养运输和大脑供氧
11:00 250ml 上午工作间隙补水,避免因轻度脱水导致的注意力下降和疲劳感
13:00 250ml 午餐后半小时喝水,帮助消化液分泌和食物消化
15:00 250ml 下午茶时间,此时人体精力开始下降,补水有助于缓解午后疲劳
17:00 250ml 下班前补水,为可能的晚间运动做准备,维持血液循环
19:00 200ml 晚餐后适量饮水,不宜过多以免影响消化
21:00 150ml 睡前少量补水,维持夜间水分平衡但避免频繁起夜

高活动量场景(9 个时段,目标 2600ml):

高活动量场景的时间表从 06:30 开始,整体时间前移,单次饮水量提升至 300-350ml,并增加了运动相关的专业提示(如"运动后补充电解质水"、“运动前预补水”、“运动过程中每 15-20 分钟补充 150-200ml”)。这体现了运动营养学中的"三阶段补水策略":运动前预补水(Pre-hydration)、运动中补水(During-exercise Hydration)、运动后补充(Rehydration)。

低活动量场景(6 个时段,目标 1600ml):

低活动量场景的时间表最精简,从 08:00 开始(允许睡懒觉的人群),单次饮水量降至 150-250ml,去除了运动相关的时段。这体现了"按需饮水"的理念——低活动量人群不需要强制饮用大量水分,适度即可。

3.2.3 场景键匹配逻辑

Mock 数据的分发逻辑基于单一维度的场景键——活动量等级(selectedActivity)。这是一个一维匹配策略,与「衣品进化室」(App1)的三维匹配(气温 × 场合 × 风格)相比,复杂度更低,但覆盖的场景组合也更少(3 种 vs 100 种)。

generateMockData(): void {
  const weight: number = parseFloat(this.weightKg);
  const activity: string = this.selectedActivity;

  // 基础饮水公式计算
  let baseMl: number = weight * 30;
  if (activity === '高') {
    baseMl = weight * 40;
  } else if (activity === '低') {
    baseMl = weight * 25;
  }

  let goalMl: number = Math.round(baseMl / 100) * 100;

  // 基于活动量场景键分发 Mock 数据
  if (activity === '中') {
    this.outputData = { /* 中活动量饮水计划 */ };
  } else if (activity === '高') {
    this.outputData = { /* 高活动量饮水计划 */ };
  } else {
    this.outputData = { /* 低活动量饮水计划(兜底) */ };
  }
}

这种 if-else 链式分发在场景数较少(3 种)时是最简洁的实现方式。如果未来需要扩展更多维度(如年龄、性别、季节),可以考虑使用 Map 或策略模式进行重构,但当前场景下,if-else 的代码可读性是最好的。

3.3 AI API 调用桩模式

「水滴打卡站」预留了一个被注释的 AI API 调用方法,这是 AI40 工具箱中所有应用的标准模式:

// async callAIAPI(): Promise<void> {
//   const response = await fetch('https://api.example.com/water', {
//     method: 'POST',
//     header: { 'Content-Type': 'application/json' },
//     extraData: JSON.stringify({
//       weight_kg: parseFloat(this.weightKg),
//       activity: this.selectedActivity
//     })
//   });
//   const result: DrinkOutput = await response.json();
//   this.outputData = result;
// }

这个 API 桩的设计有几个值得注意的细节:

  1. 返回类型显式标注Promise<void> 而非 Promise<DrinkOutput>,因为方法内部直接将结果赋值给 this.outputData 而非返回。这是 ArkTS 对函数返回类型推断限制的应对——当返回类型被省略时,如果 return 语句中的表达式是对返回类型被省略的函数或方法的调用,会发生编译时错误。因此显式指定 Promise<void> 是必要的。

  2. extraData 而非 body:在 HarmonyOS 的 @ohos.net.http 模块中,POST 请求体使用 extraData 字段而非标准 Fetch API 的 body 字段。这是鸿蒙平台 API 与 Web 标准 API 的差异之一,需要特别注意。

  3. 类型安全的 JSON 解析const result: DrinkOutput = await response.json() 使用了显式类型标注,确保 AI 返回的 JSON 数据结构与前端接口定义一致。在生产环境中,这里还需要添加运行时类型校验(如 JSON Schema 验证),因为 AI 的输出并非总是可靠的。

  4. 异步方法的切换点:在 onGenerate() 方法中,当前使用 setTimeout 模拟异步延迟,未来接入 AI API 时,只需将 setTimeout 回调中的 this.generateMockData() 替换为 this.callAIAPI(),并在 catch 块中添加降级逻辑。这种设计使得从 Mock 到 API 的切换成本极低。


四、Approve 审批阶段:ArkTS 编译合规性审计

4.1 ArkTS 严格模式合规性审计

在审批阶段,我们对「水滴打卡站」的代码进行了全面的 ArkTS 严格模式合规性审计。以下是逐项检查结果:

4.1.1 类型系统合规性
检查项 规则要求 代码实现 合规状态
禁止 any/unknown 所有类型必须显式标注 所有接口字段、方法参数、返回值均已标注 通过
禁止解构赋值 不能使用 const { a, b } = obj 使用逐字段赋值:const weight = parseFloat(this.weightKg) 通过
禁止索引访问类型 不能使用 Type[key] 未使用 通过
禁止条件类型别名 不能使用 T extends U ? X : Y 未使用 通过
接口字段类型标注 接口中所有字段必须有类型 DrinkSchedule 和 DrinkOutput 所有字段均已标注 通过
函数返回类型 显式指定返回类型 aboutToAppear(): void, onGenerate(): void, generateMockData(): void 通过
4.1.2 语法合规性
检查项 规则要求 代码实现 合规状态
禁止 var 必须使用 let 全部使用 let(如 let baseMl, let goalMl) 通过
禁止 for…in 不能使用 for…in 遍历 使用 ForEach 组件和传统 for 循环 通过
禁止嵌套函数 嵌套函数必须使用箭头函数 所有回调使用 (): void => { … } 通过
禁止函数表达式 必须使用箭头函数 所有 onClick 回调使用 lambda 通过
禁止 this 在独立函数中 this 仅限实例方法 所有 this 使用均在实例方法中 通过
禁止 export = 使用普通 export/import 使用 import { router } from ‘@kit.ArkUI’ 通过
import 语句置顶 import 必须在所有语句之前 import 语句位于文件第 1 行 通过
4.1.3 组件合规性
检查项 规则要求 代码实现 合规状态
Scroll 单子组件 Scroll 只能有一个直接子组件 Scroll 内只有一个 Column 通过
@State 命名 避免与内置属性冲突 使用 weightKg(非 weight) 通过
对象字面量 必须对应显式声明的类/接口 outputData 赋值时使用符合 DrinkOutput 接口的对象字面量 通过
数组字面量类型推断 所有元素必须可推断类型 schedule 数组元素均为 DrinkSchedule 类型 通过
不支持 JSX 不使用 JSX 表达式 全部使用 ArkUI 声明式语法 通过
4.1.4 值得关注的潜在问题

虽然代码通过了编译,但在审批阶段我们识别出以下潜在问题:

问题 1:showActivityPicker 不是 @State 变量

showActivityPicker 使用 private 修饰符而非 @State,其变化依赖于 selectedActivity 的 @State 更新来间接触发 UI 重新渲染。如果未来修改代码使得 showActivityPicker 的更新独立于 selectedActivity(例如,点击空白区域收起下拉框),下拉选择器将无法正常响应。

建议修复:showActivityPicker 改为 @State showActivityPicker: boolean = false;

问题 2:体重输入无校验

parseFloat(this.weightKg) 在用户输入非数字字符时返回 NaN,但代码中没有 isNaN 检查。NaN 乘以任何系数仍然是 NaN,导致 Math.round(NaN / 100) * 100 结果为 NaN,最终在 UI 中显示 “NaNml”。

建议修复: 在 generateMockData() 开头添加:

const weight: number = parseFloat(this.weightKg);
if (isNaN(weight) || weight <= 0) {
  this.outputData = null;
  return;
}

问题 3:无明显错误提示

当输入无效时,应用静默地不生成结果,但没有向用户展示错误提示。用户可能困惑为什么点击"AI 生成"后没有任何反应。

建议修复: 添加一个 @State errorMessage: string = '' 状态变量,在输入无效时展示错误提示。

4.2 代码质量审查

除了 ArkTS 编译合规性,我们还从代码质量角度进行了审查:

代码结构质量: 代码结构清晰,方法职责单一。generateMockData() 负责数据生成,onGenerate() 负责状态管理,build() 负责 UI 渲染。每个方法不超过 60 行,可读性良好。

命名规范: 接口使用 PascalCase(DrinkSchedule、DrinkOutput),变量使用 camelCase(weightKg、selectedActivity),方法使用 camelCase(generateMockData、onGenerate),符合 TypeScript 命名惯例。

代码复用: 活动量选择器的下拉选项列表使用了 ForEach 组件,避免了重复写三个选项的 UI 代码。但三种活动量场景的 Mock 数据中存在大量重复的 schedule 结构,如果未来场景数量增加,建议抽取公共的时间表生成逻辑。

主题色一致性: 整个应用统一使用 #4ECDC4(青绿色)作为主题色,用于按钮、LoadingProgress、饮水量文字、目标值等关键元素,视觉一致性良好。


五、Automate 自动化执行阶段:关键实现细节

5.1 活动量选择器实现

活动量选择器是「水滴打卡站」中交互最复杂的 UI 组件,它实现了一个自定义的下拉选择器(而非使用 ArkUI 原生的 Select 组件)。选择使用自定义实现而非原生组件的原因有两点:一是原生 Select 组件在 鸿蒙PC 端的样式定制能力有限,二是自定义实现可以更好地控制选中状态的高亮效果和交互反馈。

// 选择器触发区:显示当前选中值和下拉箭头
Row() {
  Text(this.selectedActivity)
    .fontSize(14)
    .fontColor('#333333')
    .layoutWeight(1)
  Text('▼')
    .fontSize(12)
    .fontColor('#8E8E93')
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(8)
.border({ width: 1, color: '#D1D1D6' })
.onClick((): void => {
  this.showActivityPicker = !this.showActivityPicker;
})

// 下拉选项列表:条件渲染
if (this.showActivityPicker) {
  Column() {
    ForEach(this.activityOptions, (item: string): void => {
      Row() {
        Text(item)
          .fontSize(14)
          .fontColor(item === this.selectedActivity ? '#007AFF' : '#333333')
          .layoutWeight(1)
        if (item === this.selectedActivity) {
          Text('✓')
            .fontSize(16)
            .fontColor('#007AFF')
        }
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 10, bottom: 10 })
      .backgroundColor(item === this.selectedActivity ? '#F0F8FF' : '#FFFFFF')
      .onClick((): void => {
        this.selectedActivity = item;
        this.showActivityPicker = false;
      })
    })
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .borderRadius(8)
  .border({ width: 1, color: '#D1D1D6' })
  .margin({ top: 4 })
}

这个选择器的实现有几个值得注意的设计细节:

选中状态的视觉反馈: 当前选中项使用蓝色文字(#007AFF)+ 浅蓝色背景(#F0F8FF)+ 对勾图标(✓),形成三重视觉反馈,确保用户能清晰识别当前选择。这种多层次反馈在无障碍设计(Accessibility)中尤为重要——仅依赖颜色变化可能对色弱用户不友好,而文字颜色 + 背景色 + 图标的三重反馈提供了足够的冗余信息。

点击选项后自动收起:onClick 回调中同时设置了 this.selectedActivity = itemthis.showActivityPicker = false,确保用户选择后下拉列表立即收起,减少不必要的交互步骤。

ForEach 的 void 返回类型: ForEach(this.activityOptions, (item: string): void => { ... }) 中,箭头函数的返回类型显式标注为 void。这不是 ArkTS 的强制要求,但保持了一致的代码风格——所有回调函数都显式指定返回类型。

5.2 饮水时间表卡片设计

饮水时间表是结果展示的核心部分,每个时段渲染为一个独立的卡片组件:

ForEach(this.outputData.schedule, (item: DrinkSchedule): void => {
  Row() {
    Column() {
      Text(item.time)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
      Text(item.ml)
        .fontSize(12)
        .fontColor('#4ECDC4')
        .margin({ top: 2 })
    }
    .width(60)
    .alignItems(HorizontalAlign.Center)

    Text(item.tip)
      .fontSize(13)
      .fontColor('#666666')
      .layoutWeight(1)
      .margin({ left: 12 })
      .maxLines(2)
      .textOverflow({ overflow: TextOverflow.Ellipsis })
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .borderRadius(8)
  .padding({ left: 12, right: 12, top: 10, bottom: 10 })
  .margin({ bottom: 6 })
  .border({ width: 1, color: '#E5E5EA' })
})

卡片布局采用"左侧固定宽度 + 右侧自适应"的模式:

  • 左侧固定区域(width: 60px):使用 Column 垂直排列时间和饮水量。时间使用粗体深色文字(#333333),饮水量使用主题色(#4ECDC4)小字,形成视觉层次。
  • 右侧自适应区域(layoutWeight: 1):使用 Text 展示饮水提示文字,设置 maxLines(2) 限制最多两行,超出部分使用省略号截断。这保证了在 鸿蒙PC 端宽屏和小屏手机上的展示一致性。

layoutWeight 的使用: layoutWeight(1) 是 ArkUI 中实现弹性布局的关键属性,它类似于 CSS 的 flex: 1,表示该组件占据剩余空间。在 Row 容器中,左侧 Column 占 60px 固定宽度,右侧 Text 占据剩余所有空间,这确保了不同屏幕尺寸下的自适应布局。

5.3 主题色选择与视觉设计

「水滴打卡站」选择了 #4ECDC4(青绿色/Teal)作为主题色,这是一个经过深思熟虑的设计决策:

色彩心理学: 青绿色是水的颜色,与"饮水"主题天然契合。同时,青绿色在色彩心理学中代表清新、健康、生命力,与健康管理类应用的定位一致。

对比度与可访问性: #4ECDC4 在白色背景上的对比度约为 3.2:1,在浅绿色背景(#F0FDF9)上的对比度约为 2.8:1。虽然这些值未达到 WCAG AA 标准(4.5:1),但考虑到主题色主要用于装饰性元素(而非正文)且字号较大(20px),在视觉上仍然清晰可辨。

与其他应用的差异化: AI40 工具箱中不同应用使用不同的主题色来建立视觉区分——App1 使用 #FF6B6B(珊瑚红,穿搭)、App4 使用 #45B7D1(天蓝,时间管理)、App11 使用 #007AFF(系统蓝,早餐)。App7 的 #4ECDC4 在色相环上介于蓝色和绿色之间,与其他应用形成清晰区分。

5.4 Loading 状态处理

Loading 状态是 AI 应用的关键体验环节,它需要在"等待"和"反馈"之间取得平衡:

onGenerate(): void {
  this.isLoading = true;
  this.hasResult = false;
  setTimeout((): void => {
    this.generateMockData();
    this.isLoading = false;
    this.hasResult = true;
  }, 800);
}

800ms 的延迟选择: 800ms 是一个经过精心选择的延迟时间。太短(如 200ms)会让用户感觉"AI 在敷衍",太快的结果生成会降低 AI 能力的可信度。太长(如 2000ms)会让用户产生焦虑感,尤其在移动端场景下。800ms 恰好处于"稍微等一下"的心理舒适区,既能营造"AI 正在计算"的真实感,又不会让用户失去耐心。

Loading 动画的视觉设计: LoadingProgress 组件的颜色设置为 #4ECDC4(主题色),与整体 UI 保持一致。配合"AI正在生成饮水计划…"的文字提示,形成了"动画 + 文字"的双重反馈机制。在未来的 鸿蒙Flutter框架 集成中,可以进一步使用 Lottie 动画实现更生动的水滴加载动画。

状态重置:onGenerate() 开头将 hasResult 重置为 false,确保在生成过程中上一次的结果不会残留显示。这是一种防御性编程实践——即使上一次的结果理论上已经被覆盖,显式重置状态可以避免任何边界情况下的 UI 闪烁。


六、Assess 评估阶段:成果评估与技术反思

6.1 功能完成度评估

功能指标 完成状态 实际表现
体重输入 完成 TextInput 数字键盘,默认值 65kg
活动量选择 完成 三种活动量,自定义下拉选择器
饮水公式计算 完成 三种活动量 × 可变体重,公式准确
目标值取整 完成 按 100ml 取整
分时段时间表 完成 中活动量 8 时段,高活动量 9 时段,低活动量 6 时段
饮水建议 完成 每种活动量有独立的专业建议
Loading 状态 完成 800ms 延迟 + LoadingProgress 动画
空状态引导 完成 未生成时展示引导文字
返回导航 完成 router.back() 正常返回
AI API 预留 完成 注释的 callAIAPI() 方法
编译合规 完成 零 Error 通过 ArkTS 严格模式

6.2 可扩展性分析

短期扩展(1-2 个迭代):

  1. 性别参数:男性和女性的每日推荐饮水量不同(男性约 1700ml,女性约 1500ml),在饮水公式中增加性别系数可以进一步提升个性化程度。
  2. 季节参数:夏季高温环境下人体水分流失更快,需要增加饮水系数。冬季则相反。
  3. 输入校验:添加体重输入的最小值(30kg)和最大值(200kg)限制,以及非数字输入的提示。

中期扩展(3-5 个迭代):

  1. 饮水打卡功能:在每个时段添加"已打卡"按钮,记录用户实际饮水情况。
  2. 历史统计:展示每日/每周/每月的饮水完成率统计图表。
  3. 推送提醒:通过 HarmonyOS 的后台任务和通知 API,在设定的饮水时间发送提醒通知。
  4. AI API 接入:将 Mock 数据替换为真实的 AI 大模型调用,利用鸿蒙原生 AI 能力生成更个性化的饮水建议。

长期扩展(6 个迭代以上):

  1. 健康数据同步:与 HarmonyOS 健康数据框架集成,读取用户的运动量、心率等数据,动态调整饮水建议。
  2. 智能水杯联动:通过蓝牙或 NFC 与智能水杯设备联动,自动记录实际饮水量。
  3. 社交功能:添加好友饮水排行榜、饮水挑战赛等社交化功能,提升用户粘性。

6.3 技术经验总结

经验 1:ArkTS 严格模式下的开发体验

在 ArkTS 严格模式下开发,最大的感受是"约束即自由"。虽然禁止 any、禁止解构、禁止 for…in 等约束在初期增加了开发的心智负担,但它们在编译阶段就阻止了大量潜在的类型错误,使得代码在运行时更加稳定。特别是对于「水滴打卡站」这种数据驱动的应用,显式类型标注让 Mock 数据的结构一目了然,减少了因类型不匹配导致的调试时间。

经验 2:Mock 数据策略的场景覆盖率

「水滴打卡站」的 Mock 数据策略覆盖了 3 种活动量场景,场景覆盖率 100%(因为活动量只有 3 个枚举值)。与「衣品进化室」(App1)的 3/100 覆盖率相比,虽然绝对覆盖率更高,但这是因为参数空间更小的结果。这提示我们:Mock 数据策略的设计应该与参数空间的大小成正比——参数空间越小,越容易实现全覆盖;参数空间越大,越需要精心选择代表性的场景组合。

经验 3:字符串类型 vs 数值类型的权衡

将 weightKg 设计为 string 类型以便与 TextInput 绑定,是一个实用的权衡。但在实际开发中,这种"便利性优先"的设计需要配合严格的输入校验。当前代码缺少 isNaN 检查,这是一个潜在的 Bug。在下一个迭代中,应该在 generateMockData() 的开头添加数值合法性校验,并在 UI 中向用户展示明确的错误提示。

经验 4:@State 状态变量的粒度控制

6 个 @State 变量对于一个小型应用来说粒度适中,每个变量都有明确的职责。hasResult 和 outputData 的重复性问题可以通过未来重构为单一状态来解决——使用一个联合类型表示"无结果/加载中/有结果"三种状态,而不是用两个布尔值 + 一个可空对象。

6.4 与工具箱中同类应用的对比

「水滴打卡站」与 AI40 工具箱中的其他应用存在一些共性和差异,这些对比有助于理解其设计定位:

维度 水滴打卡站 (App7) 心流计时器 (App4) 晨光能量站 (App11)
输入参数 体重 + 活动量(2 个) 任务数 + 专注时长 + 开始时间(3 个) 准备时间 + 偏好类型(2 个)
Mock 场景数 3 种(活动量枚举) 动态计算(无限组合) 4 种(偏好类型枚举)
输出复杂度 中等(时间表 + 建议) 高(动态时间块计算) 中等(菜品列表 + 建议)
主题色 #4ECDC4(青绿) #45B7D1(天蓝) #007AFF(系统蓝)
特殊交互 自定义下拉选择器 时间解析/格式化工具方法 步进器 + 标签选择器
数学模型 饮水公式(线性) 时间累加(线性) 无(固定数据)

从对比中可以看出,「水滴打卡站」在 AI40 工具箱中处于"中等复杂度"的定位——它比纯固定数据应用(如 App11)更复杂,但比动态计算应用(如 App4)更简单。这种定位使其成为学习 ArkTS 数据驱动 UI 开发的理想入门案例。

6.5 鸿蒙生态适配思考

「水滴打卡站」作为一个健康生活类应用,在鸿蒙生态中有独特的适配价值:

鸿蒙PC 端适配: 当前代码使用 Scroll + layoutWeight 的弹性布局,天然支持不同屏幕尺寸。在 鸿蒙PC 端,窗口可以自由缩放,Scroll 组件确保内容在任何窗口尺寸下都可以滚动浏览。但 PC 端的鼠标交互与移动端的触摸交互存在差异——活动量选择器在 PC 端可能需要支持鼠标悬停效果(hover effect),当前的实现仅支持点击交互。

鸿蒙Flutter框架 集成: 如果未来需要将应用从 ArkTS 迁移到 鸿蒙Flutter框架,数据模型(DrinkSchedule、DrinkOutput)可以直接复用,但 UI 组件需要从 ArkUI 声明式语法转换为 Flutter Widget 树。两者的响应式原理不同——ArkUI 使用 @State 装饰器,Flutter 使用 StatefulWidget 和 setState()——但数据流设计思想是一致的。Mock 数据生成逻辑作为纯 Dart 函数可以完全复用。

鸿蒙分布式能力: 鸿蒙 OS 的分布式特性为「水滴打卡站」提供了独特的扩展可能性。用户的饮水计划可以在手机、平板、手表之间无缝流转——在手机上设置体重和活动量,在手表上接收饮水提醒,在平板上查看饮水统计。这种跨设备协同是鸿蒙生态的核心竞争力,也是未来版本的重要扩展方向。


七、结语

「水滴打卡站」是 AI40 智能应用工具箱中的第 7 号应用,也是健康生活赛道的代表作品。它通过简洁的交互设计(体重输入 + 活动量选择)和科学的饮水公式(体重 × 活动系数),为用户提供了一份个性化的每日科学饮水计划。

从技术实现的角度,本文完整记录了从 6A 工作流(Align-Architect-Atomize-Approve-Automate-Assess)到 ArkTS 代码实现的完整过程。在 Align 阶段,我们明确了功能边界——输入体重和活动量,输出饮水计划——以及 ArkTS 严格模式下的技术约束。在 Architect 阶段,我们设计了 DrinkSchedule 和 DrinkOutput 两个数据接口,基于 @State 的状态管理方案,以及 Scroll + Column 的组件树结构。在 Atomize 阶段,我们将开发工作分解为 11 个原子任务,并深入分析了 Mock 数据的饮水公式、场景化时间表设计和场景键匹配逻辑。在 Approve 阶段,我们进行了全面的 ArkTS 编译合规性审计,识别了 showActivityPicker 非 @State、体重输入无校验等潜在问题。在 Automate 阶段,我们详细解析了活动量选择器、饮水时间表卡片、主题色选择和 Loading 状态等关键实现细节。在 Assess 阶段,我们评估了功能完成度、可扩展性,并总结了四条核心技术经验。

「水滴打卡站」的当前实现虽然是一个"离线优先"的 Mock 数据版本,但它预留了完整的 AI API 调用接口,为未来接入鸿蒙原生 AI 大模型提供了平滑的升级路径。在鸿蒙生态的"一次开发,多端部署"理念下,该应用在 鸿蒙PC 端、手机端和平板端都能提供一致的体验。随着 鸿蒙Flutter框架 的持续演进,类似「水滴打卡站」这样的轻量级 AI 应用将拥有更丰富的跨平台开发选择。

核心要点回顾:

  1. 科学饮水公式:基于体重 × 活动系数的线性模型,参考了《中国居民膳食指南》的建议,是可解释、可验证的领域知识驱动设计。
  2. 场景化 Mock 数据:三种活动量场景的饮水时间表与人体的生物节律和日常活动节奏相匹配,而非简单的均匀分配,体现了"场景优先"的产品设计理念。
  3. ArkTS 严格模式合规:从接口定义到方法实现,全面遵循 ArkTS 的 30+ 条语法约束,零编译错误通过,为团队提供了可复用的代码模板。
  4. AI API 桩模式:预留的 callAIAPI() 方法提供了从 Mock 到 API 的无缝切换路径,降低了后续迭代的迁移成本。
  5. 弹性布局设计:Scroll + layoutWeight + 固定宽度的组合,实现了从手机到 鸿蒙PC 的自适应布局,符合鸿蒙"一次开发,多端部署"的核心理念。

随着健康意识的持续提升和 AI 技术的快速发展,个性化健康管理将成为一个快速增长的应用赛道。「水滴打卡站」作为这一赛道的探索者,在技术架构和产品设计上都为后续迭代奠定了坚实的基础。下一阶段,我们期待将真实的 AI 能力接入应用,让每一滴水都承载着科学的关怀。


项目信息: AI40 智能应用工具箱 · App7 水滴打卡站
源码路径: entry/src/main/ets/pages/app7/Index.ets
技术栈: HarmonyOS NEXT · ArkTS · ArkUI 声明式 UI · API 24
文件大小: 335 行 ArkTS 代码
开发周期: 1 个工作日(含 6A 工作流文档)

Logo

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

更多推荐