构建「治病于未然」健康管理APP:ArkTS四Tab架构与健康评分引擎实战




目录
- 项目背景与设计理念
- 整体架构:四Tab布局设计
- 数据模型与常量体系
- 健康评分引擎
- Tab 1 — 总览页:数据看板设计
- Tab 2 — 体检页:追踪器实现
- Tab 3 — 指标页:数值可视化
- Tab 4 — 养生页:知识库与筛选
- API 24 深度兼容实践
- 编译与性能优化
- 总结与拓展
1. 项目背景与设计理念
1.1 「治病于未然」的含义
成语"治病于未然"出自《黄帝内经》:
“是故圣人不治已病治未病,不治已乱治未乱,此之谓也。”
意思是高明的医生在疾病发生之前就进行预防,而不是等到疾病发作后才治疗。这个理念在当代健康管理中尤为重要——定期体检、监测指标、养生调理,都是"治未病"的具体实践。
1.2 为什么需要这个APP?
市面上的健康管理 APP 分两种极端:
- 极端复杂型:对接医院系统、AI 诊断、电子病历,使用门槛极高
- 极端简单型:只有步数计、喝水提醒,信息密度太低
我们需要的是一份折中方案——在手机本地就能完成的核心健康管理功能:
| 用户需求 | 我们的实现 |
|---|---|
| 了解自己的整体健康水平 | 健康评分引擎 + 综合看板 |
| 知道该做哪些体检 | 18 项体检清单 + 分类筛选 |
| 查看各项指标是否正常 | 8 项核心指标 + 参考范围 + 状态颜色 |
| 学习养生知识 | 12 条四时养生贴士 + 季节筛选 |
1.3 设计原则
在动手编码之前,我们定下了三条铁律:
- 数据全固定,零后端:所有数据硬编码在
const数组中,免去数据库、网络、持久化的复杂度 - API 24 严格兼容:不使用索引签名、
Object.keys、@Builder内声明语句等受限特性 - 四 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] 区间 - 分母
totalCheckups和HEALTH_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 健康格言卡
底部的《黄帝内经》引用不仅仅是为了"好看",它承担了两个重要的产品职责:
- 文化锚点:让用户理解 APP 名称的来源和哲学依据
- 行为引导:引出"预防胜于治疗"的核心价值观
引文排版采用缩进 + 右对齐作者的形式,模拟书籍的引用格式。
6. Tab 2 — 体检页:追踪器实现
6.1 功能需求
体检页需要解决三个核心问题:
- “我该做哪些体检?” → 完整的体检清单
- “哪些我做了?哪些还没做?” → 完成度追踪
- “某项体检是做什么的?” → 展开查看详情
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 设计理念
指标页不追求复杂的图表(折线图、雷达图等),而是用最简洁的方式展示每项指标的当前值和状态。原因:
- API 24 下图表库有限,手写绘制增加了不必要的复杂度
- 用户的真实需求是"看一眼就知道哪些指标正常、哪些需要关注",不是专业的统计分析
- 固定数据只有一次测量值,不适合绘制趋势图
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. 计算属性避免重复计算
我们的 filteredCheckups 和 filteredTips 是 getter 而不是普通函数。在 ArkTS 中,getter 在每次 @State 变化时自动重新求值,但不会重复执行——ArkUI 框架会缓存和比较依赖项。
10.3 调试经验
如果编译失败,最常见的错误来自:
- @Builder 内的声明语句:检查所有 @Builder 中是否有
let或非 UI 组件语句 - 索引签名:搜索
[key:或{ [模式 - switch 语句:全局搜索
switch关键字 - 箭头函数:检查
(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 希望改变的是:从"病了再治"到"定期体检、关注指标、顺时养生"。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)