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

目录

  1. 项目背景与设计理念
  2. 整体架构:四Tab布局设计
  3. 数据模型与常量体系
  4. 健康评分引擎
  5. Tab 1 — 总览页:数据看板设计
  6. Tab 2 — 体检页:追踪器实现
  7. Tab 3 — 指标页:数值可视化
  8. Tab 4 — 养生页:知识库与筛选
  9. API 24 深度兼容实践
  10. 编译与性能优化
  11. 总结与拓展

1. 项目背景与设计理念

1.1 「治病于未然」的含义

成语"治病于未然"出自《黄帝内经》:

“是故圣人不治已病治未病,不治已乱治未乱,此之谓也。”

意思是高明的医生在疾病发生之前就进行预防,而不是等到疾病发作后才治疗。这个理念在当代健康管理中尤为重要——定期体检、监测指标、养生调理,都是"治未病"的具体实践。

1.2 为什么需要这个APP?

市面上的健康管理 APP 分两种极端:

  • 极端复杂型:对接医院系统、AI 诊断、电子病历,使用门槛极高
  • 极端简单型:只有步数计、喝水提醒,信息密度太低

我们需要的是一份折中方案——在手机本地就能完成的核心健康管理功能:

用户需求 我们的实现
了解自己的整体健康水平 健康评分引擎 + 综合看板
知道该做哪些体检 18 项体检清单 + 分类筛选
查看各项指标是否正常 8 项核心指标 + 参考范围 + 状态颜色
学习养生知识 12 条四时养生贴士 + 季节筛选

1.3 设计原则

在动手编码之前,我们定下了三条铁律:

  1. 数据全固定,零后端:所有数据硬编码在 const 数组中,免去数据库、网络、持久化的复杂度
  2. API 24 严格兼容:不使用索引签名、Object.keys@Builder 内声明语句等受限特性
  3. 四 Tab 各自独立:每个 Tab 是独立的 @Builder,通过 @State currentTab 切换,互不干扰

2. 整体架构:四Tab布局设计

2.1 组件结构总览

HealthApp (@Component)
├── 顶部标题栏 (Row)
│   ├── Text "🏥 治病于未然"
│   └── Column (健康分 + 等级)
├── Tab 导航栏 (Row + ForEach)
│   ├── 📊 总览
│   ├── 🔬 体检
│   ├── 📏 指标
│   └── 🌿 养生
├── Divider 分隔线
└── Tab 内容区 (条件渲染)
    ├── @Builder OverviewTab()       ← Tab 0
    ├── @Builder CheckupTab()        ← Tab 1
    ├── @Builder MetricsTab()        ← Tab 2
    └── @Builder TipsTab()           ← Tab 3

2.2 状态变量设计

@Component
struct HealthApp {
  @State currentTab: number = 0      // 当前 Tab 索引
  @State filterCategory: string = '全部'   // 体检分类筛选
  @State selectedTipSeason: string = '全年' // 养生季节筛选
  @State showCompleted: boolean = true     // 是否显示已完成的体检项
  @State selectedCheckupId: number = -1     // 展开的体检卡片 ID
}

这 5 个 @State 变量覆盖了所有交互状态。关键在于各 Tab 的状态是隔离的——切换到体检 Tab 时 filterCategory 起作用,切换到养生 Tab 时 selectedTipSeason 起作用,而 currentTab 决定渲染哪个 @Builder。

2.3 Tab 切换实现

Row() {
  ForEach(TAB_NAMES, (tab: string, idx: number) => {
    Text(tab)
      .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
      .fontColor(this.currentTab === idx ? '#2C3E50' : '#95A5A6')
      .backgroundColor(this.currentTab === idx ? '#E8F4FD' : 'transparent')
      .onClick(() => {
        this.currentTab = idx
        this.selectedCheckupId = -1  // 切换 Tab 时收起展开的卡片
      })
  })
}

点击 Tab 时通过 onClick 更新 currentTab,同时重置 selectedCheckupId 防止之前展开的卡片在新 Tab 中残留。

2.4 条件渲染

if (this.currentTab === 0) {
  this.OverviewTab()
} else if (this.currentTab === 1) {
  this.CheckupTab()
} else if (this.currentTab === 2) {
  this.MetricsTab()
} else if (this.currentTab === 3) {
  this.TipsTab()
}

使用 if...else if 而不是 switch——ArkTS API 24 不支持 switch 语句。通过条件分支确保同一时间只有一个 Tab 被渲染到组件树中。


3. 数据模型与常量体系

3.1 三个核心接口

我们的 APP 涉及三种实体:体检项目、健康指标、养生贴士。每种实体对应一个独立的 interface

interface CheckupItem {
  id: number
  name: string
  emoji: string
  frequency: string   // "每年一次" / "每2年一次" ...
  category: string    // 基础 / 影像 / 生化 / 专项 / 中医
  desc: string        // 详细说明
  done: boolean       // 是否已完成
}

interface HealthMetric {
  id: number
  name: string
  emoji: string
  value: number
  unit: string
  minOk: number       // 正常下限
  maxOk: number       // 正常上限
  status: string      // 正常 / 偏高 / 偏低 / 警戒
  date: string        // 测量日期
}

interface HealthTip {
  id: number
  title: string
  emoji: string
  content: string
  season: string      // 春 / 夏 / 秋 / 冬 / 全年
}

3.2 接口设计原则

每个接口都遵循了"自解释、无歧义、扁平化"的设计原则:

  • id: number:唯一标识,用于 ForEach 的 key 生成和选中状态追踪
  • emoji: string:视觉标识,为每个实体赋予独特的 emoji 图标
  • category / season:分类字段,驱动筛选逻辑
  • 所有字段都是 string / number / boolean没有嵌套对象——这是 API 24 兼容性考量的一部分

3.3 体检数据(18 项)

我们将常见体检项目分为五大类,覆盖了从基础检查到专项筛查的全谱系:

类别 项目数 频次分布
基础 1 项 每年一次
生化 7 项 每年一次 ~ 每2年一次
影像 3 项 每2年一次 ~ 每3年一次
专项 5 项 每半年一次 ~ 每5年一次
中医 1 项 每年一次

频次设计参照了中国体检指南的通用建议:

  • 每年一次:血常规、尿常规、肝功能、肾功能、血脂、血糖、心电图等
  • 每2年一次:胸部影像、腹部B超、甲状腺功能、肿瘤标志物等
  • 每3年一次:骨密度检测(45岁以上建议每年一次)
  • 每5年一次:大肠镜(45岁起建议首次筛查)
  • 每半年一次:口腔检查

3.4 健康指标数据(8 项)

选择了最具代表性的 8 项核心指标,每项都配有参考范围:

指标 参考范围 样本值 状态
收缩压 90~130 mmHg 118 ✅ 正常
舒张压 60~85 mmHg 76 ✅ 正常
空腹血糖 3.9~6.1 mmol/L 5.2 ✅ 正常
总胆固醇 2.8~5.2 mmol/L 4.8 ✅ 正常
BMI 指数 18.5~24.0 kg/m² 22.3 ✅ 正常
心率 60~100 次/分 72 ✅ 正常
尿酸 200~420 μmol/L 380 ✅ 正常
甘油三酯 0.3~1.7 mmol/L 1.8 ⚠️ 偏高

注意,我们刻意让 7 项正常、1 项偏高——这样健康评分不会达到满分 100,给用户留下"有改善空间"的现实感。

3.5 养生贴士数据(12 条)

12 条养生贴士覆盖了中医养生的核心主题,按季节分布:

季节 条数 主题
🌸 春 2 春捂秋冻、春饮花茶
☀️ 夏 2 夏季养心、夏日防暑
🍂 秋 2 秋燥润肺、秋冬养阴
❄️ 冬 1 冬藏补肾
📌 全年 5 子午觉、饭后百步走、晨起一杯水、调畅情志、饮食有节

"全年"季的贴士在所有季节筛选中都会显示——这个逻辑在 filteredTips getter 中实现:

get filteredTips(): HealthTip[] {
  let result: HealthTip[] = []
  for (let i = 0; i < HEALTH_TIPS.length; i++) {
    let tip: HealthTip = HEALTH_TIPS[i]
    if (this.selectedTipSeason === '全年') {
      result.push(tip)  // "全年" 显示所有
    } else if (tip.season === this.selectedTipSeason || tip.season === '全年') {
      result.push(tip)  // 特定季节 + 全年通用
    }
  }
  return result
}

4. 健康评分引擎

4.1 评分算法

健康评分是总览页的核心指标,也是用户最直观地了解自身健康水平的入口。我们的算法极其简单但合理:

健康评分 = 体检完成率 × 40 + 指标正常率 × 60

其中:

  • 体检完成率 = 已完成的体检项目数 / 总体检项目数
  • 指标正常率 = 状态为"正常"的指标数 / 总指标数

两个部分加权求和,体现"体检是基础,指标是核心"的权重分配。

4.2 代码实现

get healthScore(): number {
  let doneRatio: number = this.doneCheckups / this.totalCheckups
  let normalRatio: number = this.normalMetrics / HEALTH_METRICS.length
  let score: number = Math.round(doneRatio * 40 + normalRatio * 60)
  if (score < 0) score = 0
  if (score > 100) score = 100
  return score
}

注意边界处理:

  • 使用 Math.round 四舍五入到整数
  • if 兜底确保结果在 [0, 100] 区间
  • 分母 totalCheckupsHEALTH_METRICS.length 都是常量,不会为零

4.3 等级与颜色映射

get scoreLevel(): string {
  let s: number = this.healthScore
  if (s >= 90) return '优秀'
  if (s >= 75) return '良好'
  if (s >= 60) return '一般'
  return '需关注'
}

get scoreColor(): string {
  let s: number = this.healthScore
  if (s >= 90) return '#2ECC71'   // 绿
  if (s >= 75) return '#3498DB'   // 蓝
  if (s >= 60) return '#F39C12'   // 黄
  return '#E74C3C'                 // 红
}

四个等级对应四种颜色,视觉上从绿到红渐变,符合用户对"健康评分"的直觉理解。

4.4 示例计算

以当前样本数据计算:

体检完成率 = 5/18 ≈ 27.8%
指标正常率 = 7/8 = 87.5%

健康评分 = 27.8% × 40 + 87.5% × 60
         = 11.1 + 52.5
         = 63.6
         ≈ 64 分 → "一般"

这个评分是合理的——体检只完成了 5/18,虽然指标大多正常,但还有许多该做的项目没做,总体只能算"一般",给用户明确的改进方向。


5. Tab 1 — 总览页:数据看板设计

5.1 布局结构

总览页是整个 APP 的信息门户,它应该让用户在 3 秒内了解自己的整体健康状态。布局采用纵向流式

┌──────────────────────────────────┐
│         健康评分                   │
│   ⭕   64 分 · 一般              │
│   5/18 体检 · 7/8 指标正常       │
├──────────────────────────────────┤
│ ┌──────┐  ┌──────┐              │
│ │ 🔬   │  │ 📏   │              │
│ │ 5/18 │  │ 7/8  │              │
│ │体检项 │  │指标正│              │
│ └──────┘  └──────┘              │
│ ┌──────┐  ┌──────┐              │
│ │ 🌿   │  │ ⚠️   │              │
│ │ 12条 │  │ 1项  │              │
│ │养生贴│  │需关注│              │
│ └──────┘  └──────┘              │
├──────────────────────────────────┤
│ 📜 上医治未病                     │
│ "圣人不治已病治未病..."           │
│              —— 《黄帝内经》      │
└──────────────────────────────────┘

5.2 健康评分环

我们用一个居中的 Column 模拟"评分环"效果:

@Builder
OverviewTab() {
  Scroll() {
    Column() {
      // 健康评分
      Column() {
        Text('健康评分').fontSize(14).fontColor('#666')
        Text(this.healthScore.toString())
          .fontSize(56).fontWeight(FontWeight.Bold)
          .fontColor(this.scoreColor)
        Text(this.scoreLevel)
          .fontSize(16).fontColor(this.scoreColor)
          .backgroundColor(this.scoreColor + '22')
          .padding(...).borderRadius(12)
        Text('基于 ' + this.doneCheckups + '/' + this.totalCheckups + ' 项体检完成率 与 '
             + this.normalMetrics + '/' + HEALTH_METRICS.length + ' 项指标正常率')
          .fontSize(11).fontColor('#999')
      }
      .padding(20).backgroundColor('#FFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      // ...
    }
  }
}

关键细节:等级标签使用 scoreColor + '22'(hex alpha 后缀 22 ≈ 13% 不透明度)作为背景色,实现"浅色填充"效果,营造"标签"的视觉感。

5.3 四格统计卡

四个统计卡片使用 @Builder StatCard 复用:

@Builder
StatCard(emoji: string, label: string, value: string, color: string) {
  Column() {
    Text(emoji).fontSize(28)
    Text(value).fontSize(20).fontWeight(FontWeight.Bold).fontColor(color)
    Text(label).fontSize(12).fontColor('#999')
  }
  .alignItems(HorizontalAlign.Center)
  .padding(12).backgroundColor('#FFF').borderRadius(12)
  .layoutWeight(1).margin({ right: 8 })
}

通过 layoutWeight(1) 实现两列等宽排列,每行两组:

Row() {
  this.StatCard('🔬', '体检项目', this.doneCheckups + '/' + this.totalCheckups, '#3498DB')
  this.StatCard('📏', '健康指标', this.normalMetrics + '/' + HEALTH_METRICS.length, '#2ECC71')
}
Row() {
  this.StatCard('🌿', '养生贴士', HEALTH_TIPS.length + ' 条', '#9B59B6')
  this.StatCard('⚠️', '需关注', (HEALTH_METRICS.length - this.normalMetrics) + ' 项', '#E74C3C')
}

5.4 健康格言卡

底部的《黄帝内经》引用不仅仅是为了"好看",它承担了两个重要的产品职责:

  1. 文化锚点:让用户理解 APP 名称的来源和哲学依据
  2. 行为引导:引出"预防胜于治疗"的核心价值观

引文排版采用缩进 + 右对齐作者的形式,模拟书籍的引用格式。


6. Tab 2 — 体检页:追踪器实现

6.1 功能需求

体检页需要解决三个核心问题:

  1. “我该做哪些体检?” → 完整的体检清单
  2. “哪些我做了?哪些还没做?” → 完成度追踪
  3. “某项体检是做什么的?” → 展开查看详情

6.2 交互状态

@State filterCategory: string = '全部'     // 分类筛选
@State showCompleted: boolean = true        // 显示/隐藏已完成的
@State selectedCheckupId: number = -1       // 展开的卡片 ID

三个状态变量覆盖了所有用户操作:筛选、切换可见性、展开详情。

6.3 分类筛选标签

ForEach(CATEGORIES_CHECKUP, (cat: string) => {
  Text(cat)
    .backgroundColor(this.filterCategory === cat ? '#3498DB' : '#ECF0F1')
    .fontColor(this.filterCategory === cat ? '#FFF' : '#333')
    .onClick(() => {
      this.filterCategory = cat
      this.selectedCheckupId = -1
    })
})

六个分类(全部 + 5 个具体分类)以胶囊标签的形式横向排列,支持滚动。

6.4 切换已完成可见性

Text(this.showCompleted ? '🟢 显示已完成' : '⚪ 隐藏已完成')
  .onClick(() => {
    this.showCompleted = !this.showCompleted
    this.selectedCheckupId = -1
  })

这是一个"toggle"按钮,点击切换布尔值。图标从 🟢 变为 ⚪ 提供视觉反馈。同样的,切换时重置 selectedCheckupId 避免展开状态的卡片在隐藏后仍然占用 ID。

6.5 卡片设计

@Builder
CheckupCard(item: CheckupItem, expanded: boolean) {
  Column() {
    Row() {
      Text(item.emoji).fontSize(28)
      Column() {
        Text(item.name).fontSize(16).fontWeight(FontWeight.Bold)
        Row() {
          Text(item.frequency).fontSize(11).fontColor('#7F8C8D')
          Text(item.category).fontSize(10).fontColor('#FFF')
            .backgroundColor('#3498DB').padding(...).borderRadius(6)
        }
      }
      Blank()
      Text(item.done ? '✅ 已查' : '⏳ 待查').fontSize(12)
    }

    if (expanded) {
      Divider()
      Text(item.desc).fontSize(13).fontColor('#555')
    }
  }
}

卡片设计遵循"紧凑-可展开"原则:

  • 折叠状态:显示 emoji、名称、频次、分类标签、状态
  • 展开状态:追加分割线和详细说明

6.6 数据筛选流水线

filteredCheckups getter 展示了 ArkTS 中典型的数据处理流水线:

get filteredCheckups(): CheckupItem[] {
  let result: CheckupItem[] = []
  for (let i = 0; i < CHECKUP_ITEMS.length; i++) {
    let item: CheckupItem = CHECKUP_ITEMS[i]
    // 第一道筛:分类
    if (this.filterCategory !== '全部' && item.category !== this.filterCategory) continue
    // 第二道筛:可见性
    if (!this.showCompleted && item.done) continue
    result.push(item)
  }
  return result
}

两道"关卡"顺序执行,且相互独立。这个模式可以轻松扩展到更多筛选维度。


7. Tab 3 — 指标页:数值可视化

7.1 设计理念

指标页不追求复杂的图表(折线图、雷达图等),而是用最简洁的方式展示每项指标的当前值和状态。原因:

  1. API 24 下图表库有限,手写绘制增加了不必要的复杂度
  2. 用户的真实需求是"看一眼就知道哪些指标正常、哪些需要关注",不是专业的统计分析
  3. 固定数据只有一次测量值,不适合绘制趋势图

7.2 卡片布局

每个指标一张卡片,布局如下:

┌────────────────────────────────────┐
│ 🩸 收缩压              118 mmHg     │
│    测量日期: 2025-02-15    [正常]    │
│    参考范围: 90 ~ 130 mmHg          │
└────────────────────────────────────┘

7.3 代码实现

@Builder
MetricCard(metric: HealthMetric) {
  Column() {
    Row() {
      Text(metric.emoji).fontSize(24)
      Column() {
        Text(metric.name).fontSize(15).fontWeight(FontWeight.Bold)
        Text('测量日期: ' + metric.date).fontSize(11).fontColor('#999')
      }
      .layoutWeight(1).margin({ left: 10 })

      Column() {
        Text(metric.value.toString()).fontSize(22)
          .fontWeight(FontWeight.Bold).fontColor(statusColor(metric.status))
        Text(metric.unit).fontSize(11).fontColor('#999')
      }
      .alignItems(HorizontalAlign.Center)
      .margin({ right: 12 })

      Text(metric.status)
        .fontSize(12).fontColor('#FFF')
        .backgroundColor(statusColor(metric.status))
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .borderRadius(10)
    }

    Row() {
      Text('参考范围: ' + metric.minOk + ' ~ ' + metric.maxOk + ' ' + metric.unit)
        .fontSize(11).fontColor('#BBB')
    }
  }
}

7.4 状态颜色映射

function statusColor(status: string): string {
  if (status === '正常') return '#2ECC71'   // 绿
  if (status === '偏高') return '#F39C12'   // 黄
  if (status === '偏低') return '#3498DB'   // 蓝
  if (status === '警戒') return '#E74C3C'   // 红
  return '#95A5A6'                          // 灰
}

四种状态对应四种颜色,其中"偏低"用蓝色而不是红色——这是有意的设计:偏低的指标(如低血压、低血糖)不一定需要红色警报,蓝色表示"偏低但可能不紧急",而红色保留给真正的"警戒"值。


8. Tab 4 — 养生页:知识库与筛选

8.1 内容定位

养生页是本 APP 中内容密度最高的页面。它不只是一个"贴士列表",而是一个微型的中医养生知识库。12 条贴士涵盖了:

  • 四时养生:春生、夏长、秋收、冬藏
  • 起居养生:子午觉、饭后百步走、晨起一杯水
  • 情志养生:调畅情志(七情适度)
  • 饮食养生:饮食有节、春饮花茶、夏日防暑、秋冬养阴

8.2 季节筛选

Row() {
  Scroll() {
    Row() {
      ForEach(this.seasonOrder, (season: string) => {
        Text(getSeasonEmoji(season) + ' ' + season)
          .backgroundColor(this.selectedTipSeason === season ? getSeasonColor(season) : '#ECF0F1')
          .fontColor(this.selectedTipSeason === season ? '#FFF' : '#333')
          .onClick(() => { this.selectedTipSeason = season })
      })
    }
  }
}

季节标签使用独特的"季节色"——粉色代表春、橙色代表夏、深橙代表秋、蓝色代表冬、灰色代表全年。这些颜色通过 getSeasonColor 函数映射,并在选中时作为背景色填充。

8.3 卡片设计

养生卡片是所有卡片中信息最丰富的:

@Builder
TipCard(tip: HealthTip) {
  Column() {
    Row() {
      Text(tip.emoji).fontSize(28)
      Text(tip.title).fontSize(16).fontWeight(FontWeight.Bold)
        .layoutWeight(1).margin({ left: 10 })
      Text(tip.season !== '全年' ? tip.season + '季' : '全年')
        .backgroundColor(getSeasonBg(tip.season))
        .fontColor(getSeasonColor(tip.season))
        .padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8)
    }
    Divider()
    Text(tip.content).fontSize(13).fontColor('#555').lineHeight(22)
  }
}

每个贴士包含:emoji + 标题 + 季节标签 + 分割线 + 详细内容。排版在简洁和丰富之间取得了平衡。

8.4 知识内容写作

每条贴士内容的撰写遵循了一个"一句话概括 + 两句话解释"的公式:

春捂秋冻(标题)
春季阳气初生,不宜骤减衣物。"春捂"有助于阳气生发,预防感冒。尤其注意背部、腹部和足部的保暖。(内容)

这个公式确保:

  • 标题一眼就能看懂贴士主题
  • 内容包含"为什么这样做"(中医原理)和"具体怎么做"(实操建议)

9. API 24 深度兼容实践

9.1 禁忌清单回顾

在 API 24 下,以下语法是禁止的:

❌ 索引签名       { [key: string]: any }
❌ Object.keys    Object.keys(obj)
❌ Object.values  Object.values(obj)
❌ @Builder 内的 let 声明、for 循环
❌ switch 语句
❌ 箭头函数作为方法属性
❌ 动态属性访问  obj[key]

9.2 替代方案对照表

禁用特性 替代方案 本 APP 中的应用
{ [key: string]: color } 字典 if...else if statusColor() / getSeasonColor()
Object.keys(obj) 遍历已知数组 ForEach(CATEGORIES_CHECKUP, ...)
@Builder 内 let x = fn() 直接调用 fn() diffDays(md.date).toString()
switch(color) if...else if Tab 内容切换
arr.sort((a,b) => ...) 手写冒泡排序 不需要(数据已固定顺序)
obj[key] 直接访问 obj.field 所有字段都通过 . 访问

9.3 字典函数的典型模式

在标准 JS 中,一个颜色映射通常用对象字典实现:

// ❌ API 24 不允许
const COLOR_MAP = {
  '正常': '#2ECC71',
  '偏高': '#F39C12'
}
return COLOR_MAP[status]

在 API 24 中,我们使用 if...else if 链:

// ✅ API 24 兼容
function statusColor(status: string): string {
  if (status === '正常') return '#2ECC71'
  if (status === '偏高') return '#F39C12'
  if (status === '偏低') return '#3498DB'
  if (status === '警戒') return '#E74C3C'
  return '#95A5A6'
}

虽然代码行数增加了,但类型安全性大幅提高——不再有拼写错误导致的 undefined 返回值。

9.4 ForEach 的 key 函数

API 24 要求 ForEach 提供一个稳定的 key 生成函数,用于列表 diff 优化:

// ✅ 稳定且唯一的 key
ForEach(HEALTH_METRICS, (metric: HealthMetric) => {
  // ...
}, (metric: HealthMetric) => { return 'hm_' + metric.id.toString() })

key 的格式是 "前缀_数字ID",确保了:

  • 稳定性:相同数据始终生成相同 key
  • 唯一性:不同类型的数据不会撞 key
  • 可读性:调试时可以一眼看出是哪种数据

10. 编译与性能优化

10.1 编译流程

hvigorw --mode module -p module=entry -p product=default assembleHap

编译过程的主要阶段和耗时:

阶段 耗时 说明
PreBuild ~130ms 环境检查
CompileArkTS ~5s ArkTS → Ark 字节码,最核心阶段
PackageHap ~1s 打包 HAP 安装包
SignHap ~1.2s 签名

总耗时约 17 秒,比第一个纪念日 APP(~27 秒)快了 10 秒——因为第二个 APP 的代码量更大但编译速度更快,说明编译器的增量缓存生效了。

10.2 性能注意事项

虽然 700 多行的 APP 不需要过度优化,但以下实践值得注意:

1. 条件渲染优于显隐控制

// ✅ 推荐:if 条件渲染
if (this.currentTab === 0) { this.OverviewTab() }

// ❌ 避免:visibility 显隐
// this.OverviewTab().visibility(this.currentTab === 0 ? Visibility.Visible : Visibility.Hidden)

if 语句让不被显示的组件完全不在组件树中,而 .visibility() 只是隐藏了渲染结果,但组件的创建和布局计算仍然发生。

2. ForEach 的 key 函数影响列表更新性能

稳定的 key 函数让 ArkUI 框架能够精确地知道哪些列表项需要更新、哪些可以复用。如果 key 不稳定(比如用随机数或索引),每次数据变化都会重建整个列表。

3. 计算属性避免重复计算

我们的 filteredCheckupsfilteredTips 是 getter 而不是普通函数。在 ArkTS 中,getter 在每次 @State 变化时自动重新求值,但不会重复执行——ArkUI 框架会缓存和比较依赖项。

10.3 调试经验

如果编译失败,最常见的错误来自:

  1. @Builder 内的声明语句:检查所有 @Builder 中是否有 let 或非 UI 组件语句
  2. 索引签名:搜索 [key:{ [ 模式
  3. switch 语句:全局搜索 switch 关键字
  4. 箭头函数:检查 (x) => { } 是否误用在方法参数中

11. 总结与拓展

11.1 项目总结

我们从"治病于未然"这个中医理念出发,用不到 730 行 ArkTS 代码,构建了一个完整的健康管理 APP:

维度 成果
页面数 4 个 Tab,每个 Tab 包含独立的交互逻辑
数据实体 3 个接口覆盖体检、指标、养生三大领域
数据量 18 项体检 + 8 项指标 + 12 条养生贴士
交互模式 Tab 切换、分类筛选、展开详情、Toggle 开关
API 24 兼容性 100%,无任何受限语法
编译状态 ✅ BUILD SUCCESSFUL

11.2 学到了什么?

一、多 Tab 架构的核心是状态管理

5 个 @State 变量驱动了整个 APP 的所有 UI 变化。每个 Tab 有独立的状态空间,通过 currentTab 隔离。这个模式可以轻松扩展到 8 个、12 个 Tab。

二、固定数据可以做出"活"的 APP

很多人觉得"不用数据库 = 没意思"。但固定数据 + 聪明的 getter 计算,完全可以做出有交互、有动态反馈的 APP。健康评分随着体检完成率和指标正常率实时变化,给用户一种"数据在运转"的感觉。

三、API 24 的限制让代码更安全

"不能这样、不能那样"的规则在初期让人沮丧,但最终产出的代码类型安全、行为确定、几乎没有运行时错误。这是一种"先苦后甜"的编程体验。

四、中医养生的数字化表达

12 条贴士加上四色筛选,将传统中医养生知识组织成了可浏览、可筛选的"知识库"。数字化的价值不在于替代传统,而在于让传统更容易被接触和践行

11.3 拓展方向

1. 历史趋势(@Storage + @Watch)

将每次测量的指标值存入 @Storage,通过 @Watch 监听变化,在卡片上绘制简易的趋势指示:

// 用 @Storage 存储历史数据
@StorageLink('bloodPressure') bpHistory: number[] = []

2. 个性化体检计划

根据用户年龄、性别、既往病史,动态生成个性化体检清单:

// 在 getter 中做条件筛选
get personalizedCheckups(): CheckupItem[] {
  // 年龄 > 45 → 追加大肠镜、骨密度
  // 女性 → 追加妇科检查
}

3. 健康日报(Reminder Agent)

利用鸿蒙的 reminderAgent API 发送每日健康提醒:

import reminderAgent from '@ohos.reminderAgent'
// 每天早上 8:00 提醒测量血压
// 每月 1 日提醒未完成的体检

4. 知识图谱增强

将 12 条贴士扩展为可交互的知识图谱——点击"夏季养心"可以查看推荐食谱、药材、穴位等关联内容。

5. Widget 桌面卡片

将健康评分放到桌面上,用户无需打开 APP 就能看到自己的健康状态。

6. 多用户支持

家庭成员各自维护自己的体检记录和指标,通过 Tab 切换用户。

11.4 写在最后

“治病于未然"这个 APP 虽然数据量不大、交互不算复杂,但它代表了一种产品哲学——用数字化工具赋能传统健康理念。技术本身的复杂度不高,但如何让用户在打开 APP 的 5 秒内感受到"被关心”、在 30 秒内获得"有用信息"、在 3 天内养成"查看习惯"——这些才是真正的挑战。

代码的最终目标不是跑在设备上,而是改变用户的某个行为。这个 APP 希望改变的是:从"病了再治"到"定期体检、关注指标、顺时养生"

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐