鸿蒙 ArkTS 实战:从零开发体检报告 App

本文将以一个真实的鸿蒙(HarmonyOS)应用项目为案例,从需求分析、架构设计到核心代码实现,完整拆解如何使用 ArkTS 语言开发一款"体检报告查看"应用。读者将跟随文章一步步搭建出包含历史报告列表、关键指标趋势图、报告详情、个人中心等多页面的完整应用,并理解鸿蒙应用开发中的核心概念、UI 组件体系以及路由跳转机制。

运行截图

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


一、前言:为什么选择鸿蒙 ArkTS?

随着 HarmonyOS 生态的快速扩张,越来越多的开发者开始关注这一全新的分布式操作系统。HarmonyOS 不仅在手机、平板上运行,还覆盖了智慧屏、手表、车机、智能家居等众多终端。这种"一次开发,多端部署"的能力,是它最具想象力的优势之一。

ArkTS 作为鸿蒙主推的应用开发语言,在保留 TypeScript 风格的基础上,做了三个关键强化:

第一,强化了静态类型系统。TypeScript 本身是"可选类型"的超集,但 ArkTS 在编译器层面做了更严格的约束,比如要求所有变量必须有明确类型、禁止隐式 any、禁止动态属性访问等。这样做的好处是:编译器能在开发期就捕获大量错误,运行时崩溃率显著降低。

第二,深度优化了声明式 UI。ArkUI 提供的组件都采用声明式写法,开发者只需要描述"页面应该长什么样",而无需关心"如何更新 DOM"。当状态变化时,框架会自动重渲染相关部分,让代码更接近自然语言。

第三,原生支持 HarmonyOS 的分布式能力。比如"流转"(在多个设备间无缝迁移应用状态)、“原子化服务”(免安装运行)、"统一设备发现"等,都能在 ArkTS 中以简单 API 直接调用。

本项目"体检报告查看 App"聚焦在一个相对小而美的垂直场景:用户希望随时查看自己过往的体检报告,对比多次体检之间关键指标的变化趋势,并获得直观的健康提示。这类应用看似简单,但要做到体验流畅、视觉精致,仍然需要处理不少细节:

  • 历史报告应当以列表形式清晰呈现,且支持点击进入详情;
  • 关键指标应当用趋势图直观展示,并支持点击查看大图与历史数值;
  • 页面间跳转需要借助鸿蒙的路由机制,并正确传递参数;
  • 底部 Tab 导航是绝大多数 App 的标配,能够让用户在不同功能模块间快速切换;
  • 页面状态共享需要在不引入复杂状态管理库的情况下,让多个页面读到同一份数据。

本文将围绕以上几个核心点展开,从零开始构建这款应用。读完本文后,你不仅会得到一份可运行的完整代码,更会对 ArkTS 的开发模式、组件化思想以及鸿蒙应用的工程结构有一个系统的认识。

无论你是初次接触鸿蒙生态的前端工程师,还是希望把现有 Web/小程序项目迁移到鸿蒙的开发者,相信都能从本文中有所收获。


二、项目需求与功能拆解

2.1 业务需求

体检报告查看 App 主要面向需要长期管理自己健康数据的用户。在中国,成年人每年至少会进行一次体检,少则 5 年、10 年累积下来就是 5-10 份厚厚的纸质报告。但这些报告往往被束之高阁,真正能被"二次利用"的少之又少。一款好的体检 App,应当让用户用最低的成本完成以下几件事:

  1. 历次体检报告列表:以时间倒序展示用户所有的体检记录,每条记录展示日期、医院、简要结论与关键指标摘要。
  2. 关键指标趋势图:在首页以网格形式展示血压、体重、心率、血糖、BMI 等关键指标的变化趋势,每张图都是一个 Canvas 自绘的折线图。
  3. 报告详情页:点击某条报告后进入详情,可以查看完整的体检机构信息、体检结论以及所有指标的详细数值与参考范围。
  4. 指标详情页:点击首页的某张趋势图,进入该指标的独立详情页,查看大尺寸趋势图、最新/最高/最低/平均值,以及历次数值列表。
  5. 个人中心:展示用户基本信息、健康摘要(报告数/指标数/异常项数)、常用功能入口(我的报告、健康趋势、健康目标、设置等)。
  6. 底部 Tab 导航:报告、趋势、我的三个 Tab 自由切换。

2.2 功能拆解

将上述需求拆解为可实现的功能模块:

  • 数据模型层:定义 HealthReport(单次体检报告)、MetricItem(单个指标)、MetricSnapshot(单个指标的多期数据)、状态判断函数 getMetricStatus。这一层是整个应用的基石,所有数据都从这里流出。
  • 通用组件层:自绘折线图组件 MetricChart,可复用于首页小图与详情页大图。组件化的好处是:未来如果想替换成另一种图表样式,只需改这一处即可。
  • 页面层
    • Index.ets(主页面,承载 Tabs)
    • ReportDetail.ets(报告详情)
    • MetricDetail.ets(指标详情)
    • Profile.ets(个人中心)
  • 路由与导航:通过 @ohos.router 实现页面跳转与参数传递,并在 main_pages.json 中注册所有页面。
  • UI 资源:中文字符串资源,方便后续做国际化;颜色、字号等集中在少量位置声明。

2.3 非功能性需求

除了业务功能外,我们也应当考虑一些非功能性需求:

  • 性能:首页渲染应当流畅,Canvas 重绘频率受控,不影响主线程。
  • 可维护性:所有 UI 文本应集中管理,避免散落各处的"魔法字符串"。
  • 可扩展性:未来接入后端 API 时,只需替换 SAMPLE_REPORTSKEY_METRICS 两个常量即可,组件层不需要改动。
  • 可访问性:文字大小、颜色对比度应当满足一般可读性要求;状态色用绿/红,而非仅用红。

三、开发环境与工程结构

3.1 开发环境

  • DevEco Studio:鸿蒙官方 IDE,建议使用最新稳定版(如 5.0.3 及以上)。它基于 IntelliJ IDEA 平台,对 TS/ArkTS 语法、ArkUI 组件、模拟器、调试器等都有良好的支持。
  • HarmonyOS SDK:API 12 及以上(兼容 API 9 之后的大部分能力)。建议在 SDK Manager 中下载最新的 Full SDK 与对应版本的模拟器镜像。
  • ArkTS 编译器:随 DevEco Studio 一起安装,编译阶段会做严格的类型检查。
  • 运行设备:HarmonyOS 5.0 及以上的真机,或使用远程模拟器 / 本地模拟器(推荐使用真机调试,能更准确地反映 Canvas 性能)。

3.2 工程目录

ArkTSReport/
├── AppScope/                       # 应用级资源
│   ├── app.json5
│   └── resources/base/element/
│       └── string.json
├── entry/                          # 主模块
│   ├── src/main/ets/
│   │   ├── entryability/           # 入口能力(UIAbility)
│   │   ├── entrybackupability/     # 备份能力
│   │   ├── pages/                  # 页面
│   │   │   ├── Index.ets           # 主页面(Tabs)
│   │   │   ├── ReportDetail.ets    # 报告详情
│   │   │   ├── MetricDetail.ets    # 指标详情
│   │   │   └── Profile.ets         # 个人中心
│   │   ├── components/             # 通用组件
│   │   │   └── MetricChart.ets     # 自绘折线图
│   │   └── model/                  # 数据模型
│   │       └── HealthReport.ets    # 健康报告数据
│   └── src/main/resources/         # 资源
│       ├── base/element/           # 颜色、字符串、浮点
│       ├── base/media/             # 图片
│       └── base/profile/           # profile(页面注册)
│           └── main_pages.json
└── ...

整个工程结构清晰:模型、组件、页面、路由、资源各司其职,便于后期维护与扩展。

3.3 关键配置文件

  • AppScope/app.json5:应用级配置,包含 appNameversionCodeversionName 等。
  • entry/src/main/module.json5:模块级配置,包含 mainElementpagesabilities 等。
  • entry/src/main/resources/base/profile/main_pages.json:页面路由表,所有页面必须在此注册。
  • entry/src/main/resources/base/element/string.json:字符串资源。

对于本项目,最关键的改动是 main_pages.json:每新增一个页面,都要在此处添加。


四、数据模型设计

4.1 为什么先设计数据模型?

良好的数据模型是应用稳定运行的基础。在 ArkTS 中,由于类型系统是静态的,预先定义好接口可以让编译器帮助我们捕获错误,提升代码可读性。设想一下,如果整页代码都在直接操作 any 类型对象,那将是一场灾难:IDE 没法补全,重构时无法批量改字段名,运行时容易出现拼写错误。

我们定义了如下几个核心接口:

// 单个指标
export interface MetricItem {
  name: string;       // 指标名称:身高、体重、血压...
  value: number;      // 当前数值
  unit: string;       // 单位:cm、kg、mmHg...
  normalLow: number;  // 正常下限
  normalHigh: number; // 正常上限
}

// 单个指标的多期数据(用于趋势图)
export interface MetricSnapshot {
  name: string;
  unit: string;
  values: number[];   // 按时间顺序
  normalLow: number;
  normalHigh: number;
}

// 单次体检报告
export interface HealthReport {
  date: string;       // 体检日期
  hospital: string;   // 体检机构
  summary: string;    // 体检结论
  metrics: MetricItem[]; // 各项指标
}

设计上有几个要点:

  • 统一单位:所有数值都使用国际标准单位(kg、mmHg、mmol/L 等),避免展示时混淆。
  • normalLow / normalHigh:通过参考范围对每个指标做"正常/偏高/偏低"的状态判断。即使不显示参考值,前端也能算出状态。
  • 分离多期数据:单次报告用 MetricItem[] 表达全部指标;趋势图用 MetricSnapshot 表达某个指标的多次数据,这样两套数据结构互不干扰。前者关心"一次体检有哪些指标",后者关心"某个指标的历史走势"。
  • 字符串日期:日期用 ISO 格式(YYYY-MM-DD)的字符串存储,便于排序与展示。

4.2 状态判断函数

export function getMetricStatus(value: number, low: number, high: number): string {
  if (low === 0 && high === 0) {
    return '正常';
  }
  if (value < low) {
    return '偏低';
  }
  if (value > high) {
    return '偏高';
  }
  return '正常';
}

该函数非常小巧,但被广泛使用:报告卡上指标的颜色、详情页的状态文字、趋势图角标、首页异常项统计都依赖它。把它抽成纯函数的好处是:测试简单、易于复用、未来加新规则(比如"严重偏高")时只改这一处。

需要注意的是:normalLow === 0 && normalHigh === 0 表示"该指标没有标准参考范围"(比如身高、体重),这种指标我们直接判定为"正常"。

4.3 示例数据

为了让应用一启动就能看到内容,我们准备了一组模拟数据:5 次体检(2023-06 至 2025-12)、6 个关键指标。每次体检的指标都合理变化,比如血压从偏高逐渐改善,体重先升后降,呈现出真实健康管理的"曲线感"。

export const SAMPLE_REPORTS: HealthReport[] = [ /* 5 条历史报告 */ ];
export const KEY_METRICS: MetricSnapshot[] = [ /* 6 个指标的多次数据 */ ];

实际项目中,这些数据应当来自后端 API 或本地数据库。本项目为演示用,直接在模型文件中以常量形式提供。未来接入真实数据时,可以将这两个常量替换为异步加载的 Promise,并在 aboutToAppear 中赋值给 @State 变量。

4.4 关于"指标"的思考

在设计指标时,我们刻意把"身高""体重"这种没有参考范围的指标也归入同一模型,通过 normalLow = 0, normalHigh = 0 来标记。这样做有两个好处:

  1. 代码统一:UI 不需要为"有参考值"和"无参考值"分别写渲染分支,只需根据 getMetricStatus 返回值判断要不要显示状态文字。
  2. 未来扩展:如果某天医院新增了身高的"标准体重对照表",只需修改 getMetricStatus 函数即可,不需要改数据结构。

五、自绘折线图组件:MetricChart

5.1 为什么不用第三方图表库?

鸿蒙生态中有不少优秀的图表库(如 MPChartHarmony、VChart 等),但本项目刻意选择自绘 Canvas 折线图,原因有三:

  1. 零依赖:无需安装额外组件,纯 API 实现,减少包体积。
  2. 可定制:可以根据设计需要调整颜色、网格、标注等细节,第三方库往往有大量配置项但仍不够灵活。
  3. 教学价值:自己实现图表能深入理解 Canvas 渲染管线和坐标变换,对其他可视化场景也有帮助。

5.2 核心思路

折线图本质上就是把一组数值映射到画布的像素坐标,然后用 moveTo / lineTo 画出来。具体步骤:

  1. 确定画布尺寸:通过 onAreaChange 回调拿到画布真实宽高,避免硬编码导致 Grid 拉伸时图表变形。
  2. 计算数据范围:找出 minmax,并向上下各扩 10%,让曲线不要顶到边框。
  3. 绘制网格:用浅灰色横线 + 数值文字,给用户视觉刻度参考。
  4. 绘制折线:用蓝色实线连接所有数据点。
  5. 绘制数据点:在每个点位置画一个实心小圆,强化数据位置感。
  6. 绘制数值:在大图版本中,将每个点的数值标在点上方,方便用户读数。

5.3 关键代码

@Component
export struct MetricChart {
  @Prop snapshot: MetricSnapshot;
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(
    new RenderingContextSettings(true)
  );
  @State canvasWidth: number = 160;
  @State canvasHeight: number = 110;

  build() {
    Column() {
      Row() {
        Text(this.snapshot.name).fontSize(15).fontWeight(FontWeight.Medium);
        Blank();
        Text(this.latestValue() + ' ' + this.snapshot.unit).fontSize(13).fontColor('#666666');
      }
      .width('100%')
      .padding({ left: 10, right: 10, top: 8, bottom: 4 });

      Canvas(this.context)
        .width('100%')
        .height(110)
        .onReady(() => { this.drawChart(); })
        .onAreaChange((_oldVal: Area, newVal: Area) => {
          this.canvasWidth = newVal.width as number;
          this.canvasHeight = newVal.height as number;
          this.drawChart();
        });

      Text(this.statusText())
        .fontSize(12)
        .fontColor(this.statusColor())
        .margin({ top: 4, bottom: 8 });
    }
    .width('100%')
    .height(180)
    .backgroundColor('#FFFFFF')
    .borderRadius(8);
  }
}

几个关键点:

  • @Prop snapshot:从父组件接收的指标数据。@Prop 表示单向同步,父组件更新后子组件会跟着重渲染。
  • @State canvasWidth/Height:必须用 @State 修饰,因为它们的变化需要触发重绘。
  • onReady:Canvas 首次挂载时调用一次。
  • onAreaChange:画布尺寸变化时调用(Grid 拉伸、屏幕旋转等场景)。
  • RenderingContextSettings(true):开启抗锯齿,让曲线和文字更平滑。

5.4 绘制函数

绘制函数 drawChart 负责把所有像素级操作集中起来:

private drawChart(): void {
  const ctx = this.context;
  const w = this.canvasWidth;
  const h = this.canvasHeight;
  if (w <= 0 || h <= 0) return;

  const padL = 28, padR = 10, padT = 10, padB = 18;
  ctx.clearRect(0, 0, w, h);

  const values = this.snapshot.values;
  if (values.length === 0) return;

  // 1. 计算坐标范围
  let minV = values[0], maxV = values[0];
  for (let i = 0; i < values.length; i++) {
    if (values[i] < minV) minV = values[i];
    if (values[i] > maxV) maxV = values[i];
  }
  if (minV === maxV) { minV -= 1; maxV += 1; }
  const range = maxV - minV;
  minV -= range * 0.1; maxV += range * 0.1;

  const chartW = w - padL - padR;
  const chartH = h - padT - padB;

  // 2. 网格
  ctx.strokeStyle = '#EEEEEE';
  ctx.lineWidth = 1;
  for (let i = 0; i <= 4; i++) {
    const y = padT + (chartH / 4) * i;
    ctx.beginPath();
    ctx.moveTo(padL, y);
    ctx.lineTo(padL + chartW, y);
    ctx.stroke();
  }

  // 3. 折线
  ctx.strokeStyle = '#1E90FF';
  ctx.lineWidth = 2;
  ctx.beginPath();
  const stepX = values.length > 1 ? chartW / (values.length - 1) : 0;
  for (let i = 0; i < values.length; i++) {
    const x = padL + stepX * i;
    const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.stroke();

  // 4. 数据点
  ctx.fillStyle = '#1E90FF';
  for (let i = 0; i < values.length; i++) {
    const x = padL + stepX * i;
    const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
    ctx.beginPath();
    ctx.arc(x, y, 3, 0, Math.PI * 2);
    ctx.fill();
  }

  // 5. 极值标注
  ctx.fillStyle = '#999999';
  ctx.font = '10vp sans-serif';
  ctx.textAlign = 'right';
  ctx.fillText(maxV.toFixed(1), padL - 4, padT + 6);
  ctx.fillText(minV.toFixed(1), padL - 4, padT + chartH + 2);
}

这段代码虽然不复杂,但包含了 Canvas 编程的几个关键技巧:

  • 状态分离:每次绘制前 clearRect 清除上次内容,否则折线会越来越粗。
  • 比例映射:通过 padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH 把数据值映射到画布 Y 坐标。这里的关键是 Y 轴在 Canvas 中是"从上到下"增长的,而我们想让"数值大"对应"位置高",所以要用 chartH - ... 翻转。
  • 响应式重绘:通过 onAreaChange 监听画布尺寸变化,保证 Grid 拉伸时图表不变形。
  • 极值扩边:通过 minV -= range * 0.1 给上下各留 10% 边距,让曲线不要顶到边框。

5.5 大图增强

MetricDetail 中,我们复用了同样的思路,但做了增强:

  • 高度提升到 220vp;
  • 添加 Y 轴刻度文字(最大值、最小值、3 个中间值);
  • 添加 X 轴日期标签(取 MM-DD 格式);
  • 在每个数据点上方标注数值;
  • 提供"最新 / 最高 / 最低 / 平均"四个统计数字辅助查看。

这种"小图概览 + 大图详查"的设计,是健康类应用最常见的趋势图表范式。

5.6 性能与渲染时机

需要注意的是,Canvas 的绘制是"命令式"的:我们调用 stroke() / fill() 时,浏览器/鸿蒙渲染引擎才会真正去画。鸿蒙的 Canvas API 内部有缓冲机制,多次连续绘制会合并提交到 GPU 线程,因此性能损失很小。

但我们也要避免不必要的重绘。比如 onAreaChange 在父容器布局稳定前可能多次触发,每次都会重画一遍。生产环境里可以用 debounce 思路做防抖(ArkTS 内置 setTimeout 可以实现简单防抖),但本项目为了代码简洁没有引入。


六、主页面:Tabs + List + Grid 的复合布局

Index.ets 是整个应用的入口,也是最复杂的一页。它通过底部 Tabs 把"报告 / 趋势 / 我的"三大功能区组织起来,每个 Tab 内部又分别使用 List 与 Grid 展示数据。

6.1 整体结构

@Entry
@Component
struct Index {
  @State currentTabIndex: number = 0;
  @State reports: HealthReport[] = SAMPLE_REPORTS;
  @State metrics: MetricSnapshot[] = KEY_METRICS;
  @State selectedDate: string = '';

  build() {
    Column() {
      this.HeaderBar();
      Tabs({ barPosition: BarPosition.End, index: this.currentTabIndex }) {
        TabContent() { this.ReportTab(); }.tabBar(this.TabBarBuilder('报告', 0));
        TabContent() { this.TrendTab(); }.tabBar(this.TabBarBuilder('趋势', 1));
        TabContent() { this.ProfileTab(); }.tabBar(this.TabBarBuilder('我的', 2));
      }
      .barMode(BarMode.Fixed)
      .barHeight(56)
      .onChange((index: number) => { this.currentTabIndex = index; });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F6FA');
  }
}

几个关键点:

  • @State:ArkTS 中响应式状态,状态变化会自动触发 UI 重新渲染。currentTabIndex 变化时,顶部标题、Tabs 高亮等所有依赖它的 UI 都会自动更新。
  • TabsbarPosition: BarPosition.End:让 Tab 栏位于底部,更符合移动端用户习惯(参考微信、淘宝等主流 App)。
  • onChange 回调:Tab 切换时同步更新 currentTabIndex,从而改变顶部标题与右上角操作。
  • barMode: BarMode.Fixed:固定 Tab 数量(本项目是 3 个),不启用滚动。

6.2 顶部标题栏

@Builder
HeaderBar() {
  Row() {
    Text(this.headerTitle())
      .fontSize(20)
      .fontWeight(FontWeight.Bold);
    Blank();
    if (this.currentTabIndex === 0) {
      Text('共 ' + this.reports.length + ' 次').fontSize(13).fontColor('#888888');
    } else if (this.currentTabIndex === 1) {
      Text(this.reports.length + ' 次记录').fontSize(13).fontColor('#888888');
    } else {
      Text('⚙️').fontSize(20)
        .onClick(() => { router.pushUrl({ url: 'pages/Profile' }); });
    }
  }
  .width('100%').height(56).padding({ left: 16, right: 16 })
  .backgroundColor('#FFFFFF');
}

headerTitle() 是一个简单的私有方法,根据当前 Tab 返回不同标题。这种"一处状态,多处响应"是 ArkTS 声明式 UI 的精髓。

Blank() 是 ArkUI 中很实用的组件:它会自动占据父容器中的剩余空间,常用于"左侧 + 右侧对齐"的布局场景。

6.3 Tab 1:报告列表

报告 Tab 的布局从上到下分三段:

  1. 最新一次报告摘要卡:直接展示最近一次体检的简要结论,方便用户一眼看到自己的健康概况,点击可进入详情。
  2. "历次体检报告"小标题:配合右侧"按时间倒序"提示。
  3. List 列表:渲染所有历史报告。

每张报告卡片包含日期、医院、结论(最多 2 行)、关键指标摘要、查看按钮。卡片整体可点击,进入 ReportDetail

List({ space: 10 }) {
  ForEach(this.reports, (report: HealthReport) => {
    ListItem() { this.ReportCard(report); }
  }, (report: HealthReport) => report.date);
}
.listDirection(Axis.Vertical)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
.padding({ left: 12, right: 12, bottom: 16 })
.layoutWeight(1);

ForEach 的第二个参数是 keyGenerator:传入 (report) => report.date,让 ArkTS 准确识别每一项,避免不必要的重建。如果省略,框架会用数组索引作为 key,但当数组顺序变化时可能导致状态错乱。

edgeEffect(EdgeEffect.Spring) 让用户滑到边界时有弹性回弹效果,提升手感。

指标摘要部分用 Flex({ wrap: FlexWrap.Wrap }) 实现自动换行:

Flex({ wrap: FlexWrap.Wrap }) {
  ForEach(report.metrics, (m: MetricItem) => {
    Row() {
      Text(m.name).fontColor('#888888');
      Text(' ' + this.formatValue(m.value) + ' ' + m.unit).fontColor('#222222');
      if (m.normalLow > 0 || m.normalHigh > 0) {
        Text(' ' + getMetricStatus(m.value, m.normalLow, m.normalHigh))
          .fontColor(getMetricStatus(m.value, m.normalLow, m.normalHigh) === '正常' ? '#27AE60' : '#E74C3C');
      }
    }
    .padding({ left: 8, right: 8, top: 4, bottom: 4 })
    .backgroundColor('#F5F6FA')
    .borderRadius(10)
    .margin({ right: 6, bottom: 6 });
  }, (m: MetricItem) => m.name);
}

这样无论每条报告有多少指标,都能优雅地排成"标签云"形式。每个标签内:浅灰指标名 + 黑色数值 + 绿色/红色状态文字。

6.4 Tab 2:趋势总览

趋势 Tab 顶部说明"近 N 次体检关键指标",然后用一个 2 列 Grid 展示所有关键指标的小图,每张图都是一个 MetricChart

Grid() {
  ForEach(this.metrics, (item: MetricSnapshot) => {
    GridItem() {
      MetricChart({ snapshot: item });
    }
    .padding(6)
    .onClick(() => {
      router.pushUrl({
        url: 'pages/MetricDetail',
        params: { name: item.name }
      });
    });
  }, (item: MetricSnapshot) => item.name);
}
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.padding({ left: 10, right: 10, bottom: 12 });

onClick 钩子挂在 GridItem 上,点击时携带 name 参数跳到 MetricDetail。这是 ArkTS 中最常见的页面跳转写法。

columnsTemplate('1fr 1fr') 表示两列等宽。1fr 是 Grid 特有的单位,表示"按比例分配剩余空间"。如果想 3 列,就写 '1fr 1fr 1fr'

趋势页底部还放了一个"健康小贴士"卡,包含 3 条小知识:盐摄入、BMI 范围、复查频率。这种"数据 + 知识"的组合,能让应用更有人情味,也体现了"健康 App"应有的价值。

6.5 Tab 3:我的(精简版)

为了避免与 Profile.ets 重复,“我的” Tab 设计成精简版:

  • 用户卡(头像 + 姓名 + 基本信息 + "编辑"按钮)
  • 数据概览(报告数 / 指标数 / 异常项数)
  • 4 宫格快捷入口(我的报告、健康趋势、健康目标、设置)
  • 关于(应用版本、帮助、隐私协议)

4 宫格中的"我的报告""健康趋势"可以切换 Tab:

.onClick(() => { this.currentTabIndex = 0; });

而"健康目标""设置"则跳转 Profile 完整页面。这样的设计兼顾了 Tab 切换的便捷性与完整功能的可访问性,是移动端常见的"小入口 + 完整页"模式。

异常项统计是个细节亮点:用 3 个不同颜色(蓝/绿/红)的小数字,让用户在第一眼就能感受到自己的"健康分"。


七、报告详情页:ReportDetail

点击报告卡片后进入 ReportDetail,通过 router.getParams() 拿到 date,再回查报告数据:

aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['date']) {
    this.date = params['date'] as string;
  }
  for (let i = 0; i < SAMPLE_REPORTS.length; i++) {
    if (SAMPLE_REPORTS[i].date === this.date) {
      this.report = SAMPLE_REPORTS[i];
      break;
    }
  }
}

aboutToAppear 是 ArkTS 组件的生命周期钩子,组件即将显示前调用。适合做参数解析、状态初始化等操作。

页面分三个卡片:

  1. 基础信息卡:日期、医院、三个统计小方块(异常项 / 正常项 / 指标总数)。
  2. 体检结论卡:完整结论文字。
  3. 详细指标卡:每条指标一行,左侧指标名+参考值,右侧数值+状态。
Column() {
  Row() {
    Column() {
      Text(m.name).fontSize(15).fontWeight(FontWeight.Medium);
      Text('参考值 ' + m.normalLow + ' - ' + m.normalHigh + ' ' + m.unit)
        .fontSize(11).fontColor('#999999');
    }.alignItems(HorizontalAlign.Start);
    Blank();
    Column() {
      Text(this.formatValue(m.value) + ' ' + m.unit)
        .fontSize(17).fontWeight(FontWeight.Medium);
      Text(getMetricStatus(m.value, m.normalLow, m.normalHigh))
        .fontSize(11)
        .fontColor(getMetricStatus(m.value, m.normalLow, m.normalHigh) === '正常' ? '#27AE60' : '#E74C3C');
    }.alignItems(HorizontalAlign.End);
  }
  .width('100%').padding({ top: 12, bottom: 12 });

  if (m !== this.report.metrics[this.report.metrics.length - 1]) {
    Divider().color('#EEEEEE').width('100%').height(1);
  }
}

异常项统计是个不错的细节:用三个不同颜色(红/绿/蓝)的小数字,让用户在第一眼就能感受到这次体检的"健康分"。

7.1 路由参数解析的最佳实践

router.getParams() 返回的类型是 Object,必须做类型断言。本项目使用 as Record<string, Object> 把整体断言为字典,再逐字段断言为具体类型。这种"双层断言"是 ArkTS 推荐的写法。

另一种更严谨的方式是定义接口:

interface RouteParams {
  date: string;
}
const params = router.getParams() as RouteParams;

但鸿蒙路由的实际返回类型包含系统字段,强制类型转换可能会失败。本项目的写法在工程实践中更稳。


八、指标详情页:MetricDetail

指标详情页是本项目的"高光时刻"——Canvas 大图 + 统计 + 历史列表,三者结合形成一个完整的指标分析视图。

8.1 参数获取

aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['name']) {
    this.metricName = params['name'] as string;
  }
  for (let i = 0; i < KEY_METRICS.length; i++) {
    if (KEY_METRICS[i].name === this.metricName) {
      this.snapshot = KEY_METRICS[i];
      break;
    }
  }
}

8.2 大图绘制

MetricChart 不同,大图在原有基础上增加了:

  • 4 条横向网格 + 刻度文字(最大、中间、最小)
  • X 轴日期标签
  • 数据点上方数值标注
  • 加粗线条(2.5 px),更醒目
// 折线
ctx.strokeStyle = '#1E90FF';
ctx.lineWidth = 2.5;
ctx.beginPath();
for (let i = 0; i < values.length; i++) {
  const x = padL + stepX * i;
  const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
  if (i === 0) ctx.moveTo(x, y);
  else ctx.lineTo(x, y);
}
ctx.stroke();

// 数值标注
for (let i = 0; i < values.length; i++) {
  const x = padL + stepX * i;
  const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
  ctx.fillStyle = '#222222';
  ctx.font = '11vp sans-serif';
  ctx.textAlign = 'center';
  const label = Number.isInteger(values[i]) ? values[i].toString() : values[i].toFixed(1);
  ctx.fillText(label, x, y - 8);
}

数值标注的位置 y - 8 表示放在点的正上方 8 像素处。需要根据点的实际半径做微调:4 像素半径 + 4 像素间距 = 8 像素。

8.3 数据摘要

this.StatBlock('最新', this.latestValue(), '#1E90FF');
this.StatBlock('最高', this.maxValue(), '#E74C3C');
this.StatBlock('最低', this.minValue(), '#27AE60');
this.StatBlock('平均', this.avgValue(), '#9B59B6');

四个数字横排,每个用不同颜色(蓝/红/绿/紫)区分,让"最高""最低"在视觉上一眼可辨。

8.4 历次记录

历次记录以列表形式呈现,时间倒序(最新在前),并标注状态(正常/偏高/偏低):

private historyRows(): HistoryRow[] {
  if (!this.snapshot) return [];
  const rows: HistoryRow[] = [];
  for (let i = this.snapshot.values.length - 1; i >= 0; i--) {
    const date = i < SAMPLE_REPORTS.length ? SAMPLE_REPORTS[i].date : ('第' + (i + 1) + '次');
    const v = this.snapshot.values[i];
    const status = this.snapshot.normalLow > 0 || this.snapshot.normalHigh > 0
      ? getMetricStatus(v, this.snapshot.normalLow, this.snapshot.normalHigh)
      : '';
    rows.push(new HistoryRow(date, Number.isInteger(v) ? v.toString() : v.toFixed(1), status));
  }
  return rows;
}

HistoryRow 是一个简单的类,专门用于 ForEach 的 key 生成和列表渲染。

8.5 平均值与极值

虽然"最新/最高/最低"是用户最关心的三个数字,但加上"平均"能让用户对自己长期健康水平有更准确的认识。举个例子:如果一个人血压经常在 130-140 之间波动,那么"最高 140" 看起来很吓人,但"平均 135" 才是更客观的参考。

在本项目里,平均值用 toFixed(1) 保留 1 位小数,比最高最低更精确一些。


九、个人中心:Profile

Profile.ets 是独立页面,与 Tab 3 互补。它提供更完整的功能入口:

  • 用户卡(含档案号)
  • 健康摘要(报告数 / 追踪指标数 / 异常项数)
  • 功能菜单(6 项:我的报告、健康趋势、健康目标、消息提醒、家庭成员、设置)
  • 退出登录按钮
private menus: MenuItem[] = [
  { icon: '📋', title: '我的报告', desc: '查看历次体检报告' },
  { icon: '📈', title: '健康趋势', desc: '关键指标趋势分析' },
  { icon: '❤️', title: '健康目标', desc: '设置个人健康目标' },
  { icon: '🔔', title: '消息提醒', desc: '复查与异常提醒' },
  { icon: '🏥', title: '家庭成员', desc: '管理家人健康档案' },
  { icon: '⚙️', title: '设置', desc: '隐私与通用设置' }
];

每项菜单通过 ForEach 渲染,行与行之间用 Divider 分隔,最后一项不显示分割线:

if (index !== undefined && index < this.menus.length - 1) {
  Divider().color('#EEEEEE').width('100%').height(1).margin({ left: 36 });
}

index 是 ForEach 提供的第二个参数,可以用来判断是否是最后一项。注意:index 在 ArkTS 中是 number | undefined,所以这里做了空检查。

9.1 菜单项设计

每个菜单项是一个"图标 + 标题 + 描述"的三段式结构:

Row() {
  Text(item.icon).fontSize(22).margin({ right: 12 });
  Column() {
    Text(item.title).fontSize(15).fontColor('#222222');
    Text(item.desc).fontSize(11).fontColor('#999999').margin({ top: 2 });
  }.alignItems(HorizontalAlign.Start).layoutWeight(1);
  Text('›').fontSize(20).fontColor('#CCCCCC');
}
  • 图标:用 emoji 表情代替 iconfont 包,省去资源文件;
  • 标题:用户最关心的操作名;
  • 描述:补充说明,让用户无需点击就能知道这个菜单是干嘛的;
  • 右箭头:强烈暗示"可点击进入",符合用户预期。

9.2 用户卡设计

用户卡是个人中心的"门面",放在最顶部:

Row() {
  Column() {
    Text('张').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#FFFFFF');
  }
  .width(60).height(60).borderRadius(30).backgroundColor('#1E90FF')
  .justifyContent(FlexAlign.Center);

  Column() {
    Text('张三').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#222222');
    Text('男 · 32 岁 · 175cm').fontSize(13).fontColor('#888888').margin({ top: 4 });
    Row() {
      Text('🆔 ').fontSize(12).fontColor('#888888');
      Text('档案号 2024-0088').fontSize(12).fontColor('#888888');
    }.margin({ top: 4 });
  }.margin({ left: 14 }).alignItems(HorizontalAlign.Start).layoutWeight(1);
}

头像用"姓 + 圆形背景"实现,既能展示身份,又不需要图片资源。档案号暗示"我们有完整的健康档案"。


十、路由与导航

10.1 路由跳转

鸿蒙提供 import router from '@ohos.router' 模块,最常用的两个 API:

// 跳转
router.pushUrl({
  url: 'pages/ReportDetail',
  params: { date: '2025-03-05' }
});

// 返回
router.back();

pushUrl 的第二个参数 params 可以传递任意可序列化的对象。本项目传字符串(日期、指标名),传对象也是支持的。

10.2 参数接收

在目标页面 aboutToAppear 中读取:

const params = router.getParams() as Record<string, Object>;
if (params && params['date']) {
  this.date = params['date'] as string;
}

10.3 main_pages.json

新创建的页面必须在 entry/src/main/resources/base/profile/main_pages.json 中注册,否则无法访问:

{
  "src": [
    "pages/Index",
    "pages/ReportDetail",
    "pages/MetricDetail",
    "pages/Profile"
  ]
}

10.4 Tab 状态同步

Tab 内部点击"我的报告"快捷入口时,需要同步 currentTabIndex

.onClick(() => { this.currentTabIndex = 0; });

@State 的变化会自动触发 UI 重新渲染,Tabs 会平滑地切换到对应 Tab。

10.5 路由方案的扩展

对于更复杂的应用,鸿蒙还提供以下路由能力:

  • router.replaceUrl:替换当前页面(适合登录后跳转首页);
  • router.clear():清空路由栈;
  • router.getState():获取当前路由信息(用于实现"二次返回退出 App"等逻辑);
  • Navigation 组件:API 12 引入的声明式路由容器,更易于做转场动画。

本项目用最基础的 pushUrl/back 已经能满足需求,但如果未来要做复杂的转场动画,建议迁移到 Navigation 组件。


十一、UI 设计要点

11.1 颜色系统

整个应用只使用了一组精心挑选的颜色:

  • 主色:#1E90FF(道奇蓝),用于链接、强调、趋势线;
  • 成功:#27AE60(绿),用于正常状态;
  • 警告:#E74C3C(红),用于异常项;
  • 中性背景:#F5F6FA(浅灰),用于页面底色;
  • 卡片背景:#FFFFFF(白),用于内容容器;
  • 文字主色:#222222;副色:#555555 / #666666 / #888888 / #999999,按重要性递减。

这种克制的色彩系统让界面看起来干净专业,也方便后续做主题切换(深色模式只需要替换这些颜色变量即可)。

11.2 间距与圆角

所有卡片统一使用 10px 圆角,内部 padding 14-16px,卡片之间间距 8-10px。这种"呼吸感"设计让信息密度不会太高,长时间查看也不易疲劳。

Material Design 与 Apple HIG 都强调"留白"的重要性。本项目卡间距选择 10px,是因为移动端屏幕空间宝贵;如果是大屏应用,可以适当放大到 12-16px。

11.3 文字层级

  • 大标题:20-22px / Bold;
  • 卡片标题:15-17px / Medium;
  • 正文:13-14px / Regular;
  • 辅助:11-12px / Regular + 灰色。

清晰的层级帮助用户快速识别信息。在 ArkTS 中,字号单位用 vp(virtual pixel)而非 px,可以保证在不同 DPI 屏幕上大小一致。

11.4 状态色与可访问性

红色(#E74C3C)表示异常,绿色(#27AE60)表示正常。但有些色盲用户可能分不清红绿,因此在文案层面也应当明确写出"偏高/偏低/正常",让色觉障碍用户也能使用。这种"颜色 + 文字"双通道传达,是无障碍设计的基本要求。


十二、性能与体验优化

12.1 Canvas 自适应

通过 onAreaChange 监听画布尺寸变化并触发重绘,让趋势图在折叠屏、平板等不同设备上都能正确显示。如果用固定坐标硬编码,在不同分辨率下会出现偏移。

12.2 ForEach keyGenerator

ForEach 提供 key 生成函数(如 (item) => item.name),让 ArkTS 准确识别每一项的身份,避免不必要的重建。当数据变化时,框架会复用相同 key 的组件,而不是销毁重建。

12.3 状态外置

页面间共享的数据(如报告列表、关键指标)放在 model 文件中以常量形式导出,避免跨页面状态同步问题。如果用全局状态管理(如 AppStorage),要小心数据更新时机,避免出现"页面 A 已经更新,页面 B 还是旧值"的问题。

12.4 按需渲染

MetricChart 只在 onReadyonAreaChange 时绘制,不参与每帧重绘,性能开销极低。这也是 Canvas 相比 SVG 的优势:SVG 重绘需要遍历整个 DOM 树,Canvas 只需要重新执行绘制命令。

12.5 数值格式化

formatValue 统一处理"整数不带小数点、浮点数保留 1 位"的显示逻辑,避免出现 5.0 这种不友好的显示。统一格式化也方便后续做"按用户偏好显示精度"等扩展。

12.6 避免不必要的重绘

ArkTS 的响应式机制会自动追踪 @State 变量的依赖关系。但有时一个 @State 会被多个 UI 引用,修改它会导致大范围重绘。比如 currentTabIndex 既影响顶部标题,又影响 Tab 高亮,又影响 Tab 内部某些条件渲染。修改它是合理的,但如果把整个 Tab 数据都放进 currentTabIndex 就不合理了。


十三、常见问题与解决方案

13.1 @Builder 不支持函数参数

ArkTS 的 @Builder 函数参数有严格限制:不支持函数(Function)类型。本项目曾尝试用 QuickItem(icon, label, callback) 写法,发现编译失败。最终改为内联实现:每个 GridItem 内直接写 Column 和 onClick。

这是个常见的"TypeScript 能写、ArkTS 编译报错"的坑。ArkTS 之所以限制函数类型,主要是为了保证 UI 渲染的可预测性——如果 UI 树里藏着函数闭包,会让框架的优化变得困难。

13.2 Canvas 不显示

最容易踩的坑是 Canvas 区域为 0(width/height 都没设置)。本项目统一使用 width('100%') + height(110),并通过 onAreaChange 拿到真实像素。如果 Canvas 完全不显示,先检查 width/height 是否有值。

13.3 路由参数类型

router.getParams() 返回的是 Object,必须用类型断言转换为具体类型。建议在断言前用 if (params && params['xxx']) 做空检查,避免运行时错误。如果忘了空检查,目标页面在直接进入(无参数)时会因为 undefined.xxx 崩溃。

13.4 页面未注册

新页面在 main_pages.json 中遗漏是新人最常犯的错误。修改后一定要检查该文件。DevEco Studio 在编译时通常会给出警告,但偶尔也会漏报。

13.5 状态文字闪烁

如果某个指标在边界值附近(比如正常上限 120,实际值 119.5、120.5 反复横跳),状态文字会在"正常/偏高"之间闪烁。生产环境里可以对状态判断加一点缓冲(比如 ±0.5 的死区),但本项目为了简化没做。


十四、可扩展方向

完成基础功能后,这个应用还有大量可扩展空间:

  1. 数据持久化:使用 @ohos.data.relationalStore@ohos.data.preferences 替换示例数据,让用户新增的报告可以本地保存。前者适合结构化数据,后者适合少量配置。

  2. 真实图表组件:把自绘 Canvas 替换为社区维护的图表库(如 vchart),获得更丰富的图表类型(柱状、饼图、雷达图等)。但要注意图表库通常体积较大,对启动时间有影响。

  3. 健康提醒:基于异常项触发系统通知,提醒用户复查。这需要用到 Notification 能力,并在 module.json5 中申请相应权限。

  4. 多用户/家庭账户:扩展为支持多个家庭成员健康档案管理。可以用一个家庭组 ID 关联多个用户。

  5. 数据导入:支持读取医院出具的 PDF 报告,解析后导入应用。鸿蒙生态里有 PDF 解析库可以使用。

  6. 云端同步:通过 HarmonyOS 的分布式能力,把数据同步到其他设备。比如在手机上看报告,平板上自动同步。

  7. AI 健康建议:接入 LLM,根据用户健康数据给出个性化建议。鸿蒙的 AI 能力 + 端侧大模型可以做到"数据不出端"。

  8. 可视化大屏:基于鸿蒙的"元服务"卡片能力,把关键指标做成桌面卡片,让用户在不打开 App 的情况下也能看到自己的健康状况。

  9. 国际化:把所有中文文案抽到 string.json 中,并提供英文版本。鸿蒙的 i18n 框架支持多语言切换。

  10. 健康知识库:把"健康小贴士"扩展为一个完整的知识库,按指标分类,提供饮食、运动、复查等全方位建议。


十五、写在最后

本项目用不到 800 行 ArkTS 代码,实现了"报告列表 + 趋势图 + 报告详情 + 指标详情 + 个人中心"的完整应用。它体现了 ArkTS 开发的核心优势:

  • 声明式 UI:用接近自然语言的方式描述界面,代码即文档;
  • 强类型系统:编译期捕获错误,运行期更稳定;
  • 组件化:自绘图表作为独立组件,可在多处复用;
  • 路由系统:页面间跳转简洁,参数传递灵活;
  • Canvas 能力:原生支持丰富的图形绘制,无需第三方依赖。

对于想入门鸿蒙应用开发的同学,本项目是一个不错的起点。它不依赖任何第三方组件,覆盖了 ArkTS 的大部分基础语法与 UI 组件,又不失实用性。希望本文能帮助你在鸿蒙开发的道路上迈出坚实的一步。

鸿蒙生态正在快速发展,应用框架、工具链、组件库都在持续完善。站在今天这个时间点,ArkTS 已经成为一种成熟的生产力工具——它既有 TypeScript 的现代化语法,又有鸿蒙的原生能力加持。无论是做企业内部应用、To C 消费品,还是探索元服务、AI Agent,都值得投入时间学习。

源码已开源在 ArkTSReport 工程下,欢迎 clone、修改、二次创作。如果觉得本文有帮助,欢迎点赞、收藏、转发,让更多开发者了解鸿蒙生态。


附录:关键文件清单

  • entry/src/main/ets/model/HealthReport.ets — 数据模型与示例数据
  • entry/src/main/ets/components/MetricChart.ets — 自绘折线图组件
  • entry/src/main/ets/pages/Index.ets — 主页面(Tabs + List + Grid)
  • entry/src/main/ets/pages/ReportDetail.ets — 报告详情
  • entry/src/main/ets/pages/MetricDetail.ets — 指标详情
  • entry/src/main/ets/pages/Profile.ets — 个人中心
  • entry/src/main/resources/base/profile/main_pages.json — 页面路由注册

附录 B:常见 ArkTS API 速查

场景 API 用途
布局 Column / Row / Stack 基础布局容器
列表 List / ListItem / ForEach 长列表渲染
网格 Grid / GridItem 等分网格
滚动 Scroll 任意内容滚动
状态 @State / @Prop / @Link / @ObjectLink 响应式数据
画布 Canvas / CanvasRenderingContext2D 2D 图形绘制
导航 router.pushUrl / router.back 页面跳转
弹窗 AlertDialog / ActionSheet / Prompt 用户交互
媒体 Image / Video / Web 多媒体展示
数据 preferences / relationalStore 本地持久化

掌握这张表,80% 的日常开发场景都能覆盖。


附录 C:推荐学习路径

对于 ArkTS 初学者,建议按以下顺序学习:

  1. 基础语法(1 周):TypeScript + 鸿蒙 API 12 文档中的"ArkTS 语言基础"章节。
  2. UI 组件(2 周):ArkUI 组件官方文档,从基础组件开始,每个写一个小 demo。
  3. 状态管理(1 周):理解 @State / @Prop / @Link 的差异。
  4. 路由与导航(3 天):多页面跳转、参数传递、转场动画。
  5. Canvas / 自定义绘制(1 周):从简单图形开始,逐步复杂化。
  6. 持久化与网络(1 周):本地数据库、HTTP 请求、WebSocket。
  7. 分布式能力(1 周):流转、跨设备调用、统一搜索。
  8. 元服务与卡片(1 周):免安装应用、桌面卡片。

按这个节奏,2-3 个月就能从入门到胜任中等复杂度的鸿蒙应用开发。


十六、测试与调试

任何一个上线的应用都离不开测试。鸿蒙应用支持传统的单元测试、UI 测试与端到端测试,下面简单介绍本项目涉及的测试思路。

16.1 单元测试

ArkTS 支持通过 ohtest 框架编写单元测试,重点测试纯函数、工具类等不依赖 UI 的逻辑。本项目中最值得测试的是 getMetricStatus 函数:

// entry/src/test/LocalUnit.test.ets
import { getMetricStatus } from '../../main/ets/model/HealthReport';

export default function testsuite() {
  test('正常范围', () => {
    expect(getMetricStatus(120, 90, 120)).assertEqual('正常');
  });
  test('偏高', () => {
    expect(getMetricStatus(125, 90, 120)).assertEqual('偏高');
  });
  test('偏低', () => {
    expect(getMetricStatus(85, 90, 120)).assertEqual('偏低');
  });
  test('无参考', () => {
    expect(getMetricStatus(170, 0, 0)).assertEqual('正常');
  });
}

通过单元测试,我们可以放心地修改 getMetricStatus 内部实现,而不必担心破坏现有行为。

16.2 UI 测试

对于组件级别的渲染测试,鸿蒙提供 UITest 框架。它可以模拟用户点击、滑动、输入等操作,并断言 UI 状态变化。本项目暂时没写 UI 测试,因为核心 UI 都是数据驱动的——只要数据正确,UI 必然正确。

16.3 调试技巧

在 DevEco Studio 中调试 ArkTS 应用时,以下技巧能事半功倍:

  • 断点调试:在 .ets 文件中点击行号设置断点,启动调试模式后会在断点处暂停,可以查看变量值、调用栈。
  • HiLog 日志:通过 console.info / console.warn / console.error 输出日志,在 DevEco 的 Log 窗口查看。
  • Inspector:在真机或模拟器上运行应用时,可以用 Inspector 工具查看 UI 树结构、组件属性、布局信息。
  • 性能分析:通过 DevEco Profiler 工具,可以查看 CPU、内存、GPU、网络等性能指标,定位卡顿、内存泄漏等问题。

16.4 常见调试场景

场景一:Canvas 不显示

先用 console.info 输出画布的 canvasWidthcanvasHeight,确认是否大于 0。如果一直是 0,说明父容器没有正确传递尺寸,需要检查 Grid 布局配置。

场景二:路由跳转失败

检查 main_pages.json 中是否注册了目标页面;检查 url 字段是否以 pages/ 开头;检查 params 是否可序列化。

场景三:状态不更新

确认变量是否用了 @State / @Prop 装饰器;确认 UI 引用了状态变量(直接引用才会追踪);确认状态变化是在主线程触发的(鸿蒙 UI 更新必须在主线程)。


十七、与其他框架的对比

很多 ArkTS 新人会问:ArkTS 比起 Flutter、React Native、SwiftUI 等其他跨端/原生框架,优势在哪里?下面简单对比一下。

17.1 ArkTS vs Flutter

Flutter 用 Dart 语言,自带渲染引擎(Skia),可以做到"像素级一致"的多端体验。ArkTS 用类 TypeScript 语法,原生调用系统组件。

  • 学习成本:ArkTS 入门更简单,前端开发者零成本上手;Dart 对大多数人来说较新。
  • 性能:Flutter 在图形密集型场景(复杂动画、自定义绘制)略胜;ArkTS 在系统集成、分布式能力上更强。
  • 生态:Flutter 生态成熟、组件丰富;ArkTS 生态正在快速建设中。
  • 包体积:Flutter 引入 Skia 后包体积偏大;ArkTS 用系统组件更轻量。

17.2 ArkTS vs React Native

React Native 用 JavaScript + JSX 桥接到原生组件。ArkTS 是更彻底的"原生"。

  • 渲染机制:React Native 仍依赖 JS 引擎 + Bridge,性能有损耗;ArkTS 编译为原生代码,无中间层。
  • 类型系统:ArkTS 强类型,TypeScript 在 React Native 中仍可能被绕过。
  • UI 范式:两者都是声明式 UI,但 ArkTS 的 @State 响应式追踪更细粒度。

17.3 ArkTS vs SwiftUI

SwiftUI 是 Apple 的声明式 UI 框架,与 ArkTS 在设计哲学上非常相似(都强调状态驱动 UI)。

  • 跨平台:SwiftUI 仅限 Apple 生态;ArkTS 覆盖鸿蒙全家桶。
  • 语言:SwiftUI 用 Swift;ArkTS 用 TypeScript-like 语法。前者学习曲线更陡。
  • 生态:SwiftUI 生态成熟,组件丰富;ArkTS 在快速追赶中。

17.4 选型建议

  • 如果应用只在鸿蒙生态内运行,首选 ArkTS
  • 如果要同时覆盖 iOS/Android,Flutter / React Native 仍然是更成熟的选择;
  • 如果是企业内部工具、需要深度集成鸿蒙能力(如分布式、原子化服务),ArkTS 是唯一选项

十八、部署与发布

开发完成后,下一步就是打包发布。鸿蒙应用的发布流程包括签名、打包、上架三个步骤。

18.1 应用签名

鸿蒙要求所有应用必须经过签名才能安装。开发阶段可以使用 DevEco Studio 提供的自动签名(AGConnect / 调试证书)。发布到应用市场前,需要:

  1. 在华为开发者联盟申请发布证书;
  2. 在 DevEco Studio 中配置 Build Profile,选择发布证书;
  3. 构建 HAP(HarmonyOS Ability Package)文件。

18.2 多端发布

鸿蒙应用支持以下发布渠道:

  • 华为应用市场(AppGallery):面向消费者的主渠道。
  • 企业内部分发:通过 MDM(Mobile Device Management)系统向企业员工推送。
  • 快应用 / 元服务:免安装形态,用户无需下载即可使用。
  • Web 部署:通过 DevEco Studio 把项目打包为 PWA / H5,在浏览器中运行(部分能力受限)。

本项目是一个普通的鸿蒙 App,可以直接打包成 HAP 上架到应用市场。

18.3 版本管理

AppScope/app.json5 中维护版本信息:

{
  "app": {
    "versionName": "1.0.0",
    "versionCode": 1
  }
}

每次发版前需要递增 versionCode(整数)与 versionName(用户可见的版本号)。

18.4 灰度发布

华为应用市场支持按比例、按地域、按机型灰度发布新版本。建议新功能先以 5% 灰度上线,观察无问题后再逐步放大。


十九、项目源码精读

在结束本文之前,我们快速过一遍项目源码的关键片段,帮助读者在本地工程中快速定位。

19.1 数据流

[model/HealthReport.ets]  -- 导出 SAMPLE_REPORTS / KEY_METRICS
        ↓
[pages/Index.ets]  -- @State reports / metrics 持有
        ↓
   ┌────┴────┐
[List/Grid]  [components/MetricChart]  -- 子组件接收
        ↓
[user click]  -- router.pushUrl 跳转
        ↓
[pages/ReportDetail / MetricDetail / Profile]  -- 独立页面

整个数据流是单向的:数据从模型流入页面,用户的交互产生新事件,事件触发跳转或状态更新。这种单向数据流让应用行为可预测、易调试。

19.2 关键文件行数

为了让你对项目体量有直观感受,列出主要文件的大致行数:

  • HealthReport.ets:约 100 行
  • MetricChart.ets:约 180 行
  • Index.ets:约 350 行
  • ReportDetail.ets:约 200 行
  • MetricDetail.ets:约 280 行
  • Profile.ets:约 150 行
  • main_pages.json / string.json / module.json5:约 80 行

总计不到 1400 行的代码(含注释),实现了一个完整的多页面应用。这正是 ArkTS 声明式 UI 的魅力:少即是多。

19.3 代码风格

本项目遵循以下代码风格:

  • 缩进:4 空格
  • 引号:单引号优先
  • 命名:组件名 PascalCase,变量/函数 camelCase,常量 UPPER_SNAKE_CASE
  • 注释:行注释用 //,块注释用 /* */,组件顶部用 // xxx:xxx 简述用途
  • 空行:函数/Builder 之间空一行
  • 类型:所有变量必须有明确类型,不使用 any

这些风格与鸿蒙官方示例保持一致,方便与社区代码交流。


二十、致谢与参考

本文写作过程中参考了以下资源:

感谢鸿蒙生态的开发者们,是你们的代码与分享让这个生态越来越丰富。


写在最后:健康是人生最宝贵的财富。希望这款小小的 App,能帮你更好地管理自己的健康数据,也希望你通过本文的旅程,对鸿蒙开发充满信心。未来的你,会感谢现在努力学习的自己。加油!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐