HarmonyOS Next 项目完整学习指南
基于 HealthyMate(泰科健康)项目的实战学习文档
适合人群: 完成 HarmonyOS Next 基础学习的初学者
学习目标: 通过真实项目巩固基础知识,掌握实际开发技能
📚 目录
- 第一章:项目概览
- 第二章:核心概念速览
- 第三章:项目结构详解
- [第四章:从零开始 - 启动流程](#第四章:从零开始 - 启动流程)
- 第五章:UI组件开发
- 第六章:状态管理
- 第七章:路由导航
- 第八章:数据模型
- 第九章:图表组件
- 第十章:最佳实践
- 第十一章:常见问题
第一章:项目概览
1.1 项目简介
**HealthyMate(泰科健康)**是一款基于 HarmonyOS Next 开发的综合性健康管理应用。
核心功能模块:
- 🏠 首页模块: 健康检测、医院查询、热点资讯
- 🔍 健康发现: 健康知识、视频播放、管理工具
- 📊 健康数据: 数据展示、趋势分析、图表可视化
- 👤 个人中心: 用户信息、设置管理
技术栈:
开发框架: HarmonyOS Next
开发语言: ArkTS (TypeScript 扩展)
UI框架: ArkUI (声明式UI)
开发工具: DevEco Studio
包管理: ohpm
1.2 学习路线图
启动流程 → UI组件 → 状态管理 → 路由导航 → 数据交互 → 高级特性
↓ ↓ ↓ ↓ ↓ ↓
Entry @Component @State Navigation Model 第三方库
Ability @Builder AppStorage router Class mpchart
第二章:核心概念速览
2.1 ArkTS 基础概念
2.1.1 装饰器系统
ArkTS 使用装饰器来增强类、组件和变量的功能:
装饰器 | 作用 | 使用场景 | 示例 |
---|---|---|---|
@Entry |
标记页面入口 | 可独立运行的页面 | 启动页、登录页 |
@Component |
定义自定义组件 | 可复用的UI组件 | 卡片、按钮 |
@State |
状态变量 | 组件内部状态 | 计数器、开关 |
@Prop |
单向数据传递 | 父组件→子组件 | 配置参数 |
@Link |
双向数据绑定 | 父子组件共享 | 表单数据 |
@Builder |
自定义构建函数 | 动态UI内容 | 插槽内容 |
@BuilderParam |
插槽参数 | 接收UI内容 | 卡片插槽 |
@Extend |
扩展组件样式 | 统一样式 | 文本样式 |
@Preview |
预览组件 | 开发调试 | 组件预览 |
2.1.2 核心组件
容器组件:
typescript
Column() // 垂直布局
Row() // 水平布局
Stack() // 堆叠布局
Flex() // 弹性布局
Grid() // 网格布局
List() // 列表布局
Swiper() // 轮播容器
Tabs() // 标签页容器
基础组件:
typescript
Text() // 文本
Image() // 图片
Button() // 按钮
TextInput() // 文本输入
2.2 项目架构概念
2.2.1 应用生命周期
应用启动
↓
EntryAbility.onCreate() // Ability 创建
↓
onWindowStageCreate() // 窗口创建
↓
loadContent('pages/SplashPage') // 加载启动页
↓
页面显示
↓
aboutToAppear() // 页面即将显示
↓
build() // 构建UI
↓
onPageShow() // 页面已显示
2.2.2 组件生命周期
typescript
@Component
struct MyComponent {
// 1. 组件即将创建
aboutToAppear() {
console.log('组件即将出现')
// 初始化数据、启动定时器
}
// 2. 构建UI(每次状态变化都会触发)
build() {
Column() {
Text('Hello')
}
}
// 3. 组件即将销毁
aboutToDisappear() {
console.log('组件即将消失')
// 清理定时器、释放资源
}
}
第三章:项目结构详解
3.1 目录树结构
HealthyMate/
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用配置文件
│ └── resources/ # 全局资源
│
├── entry/ # 主模块
│ ├── src/main/
│ │ ├── ets/ # ArkTS 源代码
│ │ │ ├── entryability/ # 应用入口
│ │ │ │ └── EntryAbility.ets
│ │ │ │
│ │ │ ├── pages/ # 页面文件
│ │ │ │ ├── Index.ets # 主页面
│ │ │ │ ├── SplashPage.ets # 启动页
│ │ │ │ ├── GuidePage.ets # 引导页
│ │ │ │ ├── LoginPage.ets # 登录页
│ │ │ │ │
│ │ │ │ ├── comp/ # 公共组件
│ │ │ │ │ ├── HomeComp.ets # 首页组件
│ │ │ │ │ ├── CardComp.ets # 卡片组件
│ │ │ │ │ ├── TopNavComp.ets # 导航栏
│ │ │ │ │ └── ...
│ │ │ │ │
│ │ │ │ ├── model/ # 数据模型
│ │ │ │ │ ├── NavCard.ets # 导航卡片模型
│ │ │ │ │ └── HealthyData.ets # 健康数据模型
│ │ │ │ │
│ │ │ │ ├── view/ # 视图组件
│ │ │ │ │ ├── LineCharts.ets # 折线图
│ │ │ │ │ └── BarCharts.ets # 柱状图
│ │ │ │ │
│ │ │ │ ├── home/ # 首页相关页面
│ │ │ │ ├── healthydiscover/ # 健康发现页面
│ │ │ │ └── profile/ # 个人中心页面
│ │ │ │
│ │ │ └── utils/ # 工具类
│ │ │
│ │ ├── resources/ # 资源文件
│ │ │ ├── base/ # 基础资源
│ │ │ │ ├── element/ # 元素资源
│ │ │ │ │ ├── string.json # 字符串资源
│ │ │ │ │ ├── color.json # 颜色资源
│ │ │ │ │ └── float.json # 尺寸资源
│ │ │ │ ├── media/ # 媒体资源(图片等)
│ │ │ │ └── profile/ # 配置文件
│ │ │ │ └── main_pages.json
│ │ │ │ └── route_map.json
│ │ │ ├── zh_CN/ # 中文资源
│ │ │ └── en_US/ # 英文资源
│ │ │
│ │ └── module.json5 # 模块配置文件
│ │
│ └── build-profile.json5 # 构建配置
│
├── oh_modules/ # 第三方依赖
│ └── @ohos/
│ └── mpchart/ # 图表库
│
├── oh-package.json5 # 包配置文件
└── hvigorfile.ts # 构建脚本
3.2 关键文件说明
3.2.1 module.json5
- 模块配置
json5
{
"module": {
"name": "entry",
"type": "entry",
"routerMap": "$profile:route_map",
"mainElement": "EntryAbility",
"pages": "$profile:main_pages",
// 权限声明
"requestPermissions": [
{
"name": "ohos.permission.INTERNET" // 网络权限
},
{
"name": "ohos.permission.LOCATION" // 位置权限
}
]
}
}
重要配置项:
mainElement
: 指定应用入口 Abilitypages
: 页面配置文件路径routerMap
: 路由配置文件路径requestPermissions
: 权限声明列表
3.2.2 main_pages.json
- 页面配置
json
{
"src": [
"pages/SplashPage",
"pages/GuidePage",
"pages/Index",
"pages/LoginPage"
]
}
3.2.3 route_map.json
- 路由配置
json
{
"routerMap": [
{
"name": "HealthCheckPage",
"pageSourceFile": "src/main/ets/pages/home/HealthCheckPage.ets"
}
]
}
第四章:从零开始 - 启动流程
4.1 应用入口:EntryAbility
文件位置 : entry/src/main/ets/entryability/EntryAbility.ets
typescript
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
// 1. Ability 创建时调用
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.log('应用创建')
}
// 2. 窗口阶段创建
onWindowStageCreate(windowStage: window.WindowStage): void {
// 加载启动页
windowStage.loadContent('pages/SplashPage', (err) => {
if (err.code) {
console.error('加载页面失败:', err)
return;
}
console.log('页面加载成功')
});
}
// 3. 前台
onForeground(): void {
console.log('应用进入前台')
}
// 4. 后台
onBackground(): void {
console.log('应用进入后台')
}
// 5. 销毁
onDestroy(): void {
console.log('应用销毁')
}
}
知识点总结:
- ✅
UIAbility
是应用/服务的基类 - ✅
onWindowStageCreate
中加载首页 - ✅ 通过
loadContent
指定启动页面
4.2 启动页实现:SplashPage
文件位置 : entry/src/main/ets/pages/SplashPage.ets
typescript
import { router } from "@kit.ArkUI";
@Entry
@Component
struct SplashPage {
@State countDown: number = 5 // 倒计时秒数
@State isAutoJump: boolean = true // 是否自动跳转
private timerID: number = 0 // 定时器ID
// 页面显示时启动定时器
aboutToAppear() {
this.startCountdown()
}
// 页面隐藏时清除定时器
aboutToDisappear() {
clearInterval(this.timerID)
}
// 启动倒计时
private startCountdown() {
this.timerID = setInterval(() => {
if (this.countDown > 0) {
this.countDown -= 1 // 每秒减1
} else {
clearInterval(this.timerID) // 清除定时器
this.jumpToHome() // 跳转
}
}, 1000)
}
// 手动跳过
private skipGuide() {
clearInterval(this.timerID)
this.jumpToHome()
}
// 跳转到引导页
private jumpToHome() {
router.pushUrl({ url: 'pages/GuidePage' })
}
build() {
Stack({ alignContent: Alignment.TopEnd }) {
// 主内容区域
Column() {
Column({ space: 12 }) {
Image($r("app.media.icon_app"))
.width(140)
.height(160)
Text('泰克健康')
.fontSize(38)
.fontColor('#000000')
.margin({ top: 10 })
Text('--- 安全守护 ---')
.fontSize(38)
.fontColor('#A5ABCE')
}
.width(238)
.height(328)
}
.justifyContent(FlexAlign.Center)
.height('100%')
.width('100%')
// 右上角跳过按钮
Button() {
Text('跳过 ' + this.countDown.toString())
.fontSize(18)
.fontColor('#FFFFFF')
}
.margin({ top: 20, right: 12 })
.padding({ left: 10, top: 5, right: 10, bottom: 5 })
.borderRadius(15)
.backgroundColor('#33000000')
.onClick(() => {
this.skipGuide()
})
}
}
}
知识点详解:
4.2.1 装饰器用法
typescript
@Entry // 标记为页面入口,可以独立运行
@Component // 标记为自定义组件
struct SplashPage { }
4.2.2 状态管理
typescript
@State countDown: number = 5
// @State 装饰的变量改变时,会自动触发UI刷新
4.2.3 生命周期钩子
typescript
aboutToAppear() // 组件即将出现(页面加载后)
aboutToDisappear() // 组件即将消失(页面销毁前)
4.2.4 定时器使用
typescript
// 启动定时器
this.timerID = setInterval(() => {
// 每秒执行
}, 1000)
// 清除定时器(重要!防止内存泄漏)
clearInterval(this.timerID)
4.2.5 Stack 布局
typescript
Stack({ alignContent: Alignment.TopEnd }) {
// 子组件会堆叠在一起
Column() { } // 第一层:主内容
Button() { } // 第二层:浮动按钮
}
Stack 对齐方式:
Alignment.TopStart
- 左上角Alignment.TopEnd
- 右上角Alignment.Center
- 居中Alignment.BottomEnd
- 右下角
4.3 引导页实现:GuidePage
文件位置 : entry/src/main/ets/pages/GuidePage.ets
typescript
import GuideComp from './comp/GuideComp'
import { router } from '@kit.ArkUI'
// 引导页数据接口
interface GuideItem {
currentIndex: number
guideText1: string
guideText2: string
guideImage: ResourceStr
isDisplayBtn?: boolean
}
// 引导页数据
const guideArrays: GuideItem[] = [
{
currentIndex: 0,
guideText1: '智能检测',
guideText2: '让健康生活触手可及',
guideImage: $r('app.media.guide01')
},
{
currentIndex: 1,
guideText1: '按时服药,健康相伴',
guideText2: '您的智能健康提醒专家',
guideImage: $r('app.media.guide02')
},
{
currentIndex: 2,
guideText1: '您的个人健康档案',
guideText2: '一目了然',
guideImage: $r('app.media.guide03'),
isDisplayBtn: true // 最后一页显示按钮
}
]
@Entry
@Component
struct GuidePage {
@State currentIndex: number = 0
@State guideList: GuideItem[] = guideArrays
build() {
Swiper() {
ForEach(this.guideList, (item: GuideItem, index) => {
Column() {
// 引导内容组件
GuideComp({
currentIndex: item.currentIndex,
guideText1: item.guideText1,
guideText2: item.guideText2,
guideImage: item.guideImage
})
// 最后一页显示按钮
if (index === this.guideList.length - 1) {
Button('立即开启', { type: ButtonType.Normal })
.onClick(() => {
router.pushUrl({ url: 'pages/RegisterPage' })
})
.padding({ top: 12, bottom: 12 })
.width('88%')
.borderRadius(12)
.linearGradient({
angle: 180,
colors: [
[0x9AE0FF, 0.0],
[0x526AF3, 1.0]
]
})
}
}
.width('100%')
.height('100%')
})
}
.loop(false) // 不循环
.autoPlay(false) // 不自动播放
.cachedCount(3) // 缓存页面数
.disableSwipe(this.currentIndex === this.guideList.length - 1)
.indicator( // 指示器样式
Indicator.dot()
.itemWidth(15)
.itemHeight(15)
.selectedItemWidth(30)
.selectedItemHeight(15)
.selectedColor(0x5F80F5)
)
.onChange((index: number) => {
this.currentIndex = index // 更新当前索引
})
}
}
知识点详解:
4.3.1 Swiper 轮播组件
typescript
Swiper() {
// 子组件
}
.loop(false) // 是否循环
.autoPlay(false) // 是否自动播放
.indicator() // 指示器
.onChange() // 页面切换回调
4.3.2 ForEach 列表渲染
typescript
ForEach(
arr: Array<T>, // 数据源
itemGenerator: (item: T, index: number) => void, // 渲染函数
keyGenerator?: (item: T, index: number) => string // 键生成函数
)
// 示例
ForEach(this.guideList, (item: GuideItem, index) => {
Text(item.guideText1)
}, (item: GuideItem) => item.currentIndex.toString())
4.3.3 条件渲染
typescript
if (condition) {
// 条件为真时显示
Button('显示')
}
4.3.4 渐变效果
typescript
.linearGradient({
angle: 180, // 渐变角度
colors: [
[Color1, 0.0], // 起始颜色和位置
[Color2, 1.0] // 结束颜色和位置
]
})
第五章:UI组件开发
5.1 主页面结构:Index
文件位置 : entry/src/main/ets/pages/Index.ets
typescript
import HomeComp from './comp/HomeComp'
import DiscoverComp from './comp/DiscoverComp'
import DataComp from './comp/DataComp'
import ProfileComp from './comp/ProfileComp'
// 设置全局状态
AppStorage.setOrCreate('CurrentTabIndex', 0);
AppStorage.setOrCreate('IsLoggedIn', false);
@Entry
@Component
struct Index {
// 创建路由栈
pathStack: NavPathStack = new NavPathStack();
aboutToAppear(): void {
// 将路由栈存储到全局
AppStorage.setOrCreate("pathStack", this.pathStack);
}
@State currentIndex: number = 0
// 底部导航图标
private navIcons = [
$r('app.media.icon_home_svg'),
$r('app.media.icon_discover_svg'),
$r('app.media.icon_data_svg'),
$r('app.media.icon_profile_svg')
]
build() {
Navigation(this.pathStack) {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
HomeComp()
}.tabBar(this.tabBarBuilder('首页', 0))
TabContent() {
DiscoverComp()
}.tabBar(this.tabBarBuilder('健康发现', 1))
TabContent() {
DataComp()
}.tabBar(this.tabBarBuilder('健康数据', 2))
TabContent() {
ProfileComp()
}.tabBar(this.tabBarBuilder('个人中心', 3))
}
.onChange((index: number) => {
this.currentIndex = index
})
}
.mode(NavigationMode.Stack)
.hideTitleBar(true)
}
// 自定义 TabBar 构建函数
@Builder
tabBarBuilder(name: string, index: number) {
Column() {
Image(this.navIcons[index])
.fillColor(this.currentIndex === index ? '#4D91FF' : '#A9B7D8')
.width(25)
.height(25)
Text(name)
.fontSize(14)
.fontColor(this.currentIndex === index ? '#677DF7' : '#A9B7D8')
}
}
}
知识点详解:
5.1.1 Tabs 标签页组件
typescript
Tabs({ barPosition: BarPosition.End }) { // 底部标签栏
TabContent() {
// 页面内容
}.tabBar(自定义TabBar)
}
.onChange((index: number) => {
// 标签切换回调
})
BarPosition 枚举:
BarPosition.Start
- 顶部BarPosition.End
- 底部
5.1.2 Navigation 导航容器
typescript
Navigation(pathStack) {
// 页面内容
}
.mode(NavigationMode.Stack) // 堆栈模式
.hideTitleBar(true) // 隐藏标题栏
5.1.3 @Builder 自定义构建函数
typescript
@Builder
tabBarBuilder(name: string, index: number) {
Column() {
Image(...)
Text(name)
}
}
// 使用
.tabBar(this.tabBarBuilder('首页', 0))
@Builder 要点:
- ✅ 可以接收参数
- ✅ 可以在
build()
方法外定义 - ✅ 可以复用UI结构
5.2 自定义组件:CardComp
文件位置 : entry/src/main/ets/pages/comp/CardComp.ets
typescript
class NavItem {
img: ResourceStr
title: string
subtitle?: string
constructor(img: ResourceStr, title: string, subtitle?: string) {
this.img = img
this.title = title
this.subtitle = subtitle
}
}
@Component
export default struct CardComp {
// 组件属性
wid: number | string = '100%'
gradientStart?: string
gradientEnd?: string
paddingValue?: Padding | Length | LocalizedPadding = 12
// 接收父组件传递的数据
@Prop params: NavItem
// 插槽参数:接收父组件传递的UI构建函数
@BuilderParam contentBuilder: (params: NavItem) => void = initBuilder
build() {
Column() {
// 调用传入的构建函数
this.contentBuilder(this.params)
}
.borderRadius(12)
.padding(this.paddingValue)
.width(this.wid)
.linearGradient(
this.gradientStart && this.gradientEnd ? {
direction: GradientDirection.Right,
colors: [
[this.gradientStart, 0.0],
[this.gradientEnd, 1.0]
]
} : { colors: [] }
)
.backgroundColor(
this.gradientStart ? Color.Transparent : '#FFFFFF'
)
}
}
// 默认构建函数
@Builder
function initBuilder() {
Text('默认占位符')
}
使用示例:
typescript
// 父组件中使用
CardComp({
wid: '100%',
gradientStart: '#CCF2FF',
gradientEnd: '#FFFFFF',
paddingValue: 5,
params: myNavItem, // 传递数据
contentBuilder: itemContent // 传递UI构建函数
})
// 自定义内容构建函数
@Builder
function itemContent(param: NavItem) {
Row({ space: 2 }) {
Image(param.img).width(44).height(44)
Text(param.title).fontSize(14)
}.width('100%')
}
知识点详解:
5.2.1 @Prop 单向数据传递
typescript
@Prop params: NavItem
// 特点:
// ✅ 父组件 → 子组件 单向传递
// ✅ 子组件修改不影响父组件
// ✅ 支持深拷贝
5.2.2 @BuilderParam 插槽
typescript
@BuilderParam contentBuilder: (params: NavItem) => void = defaultBuilder
// 特点:
// ✅ 允许父组件自定义子组件的部分UI
// ✅ 类似 Vue 的 slot、React 的 children
// ✅ 可以传递参数
@BuilderParam 传参规则:
typescript
// ❌ 错误:直接调用
contentBuilder: this.myBuilder()
// ✅ 正确:传递函数引用
contentBuilder: this.myBuilder
// ✅ 正确:传递全局Builder
contentBuilder: globalBuilder
5.2.3 条件样式
typescript
.linearGradient(
condition ? styleA : styleB
)
// 示例
.backgroundColor(
this.gradientStart ? Color.Transparent : '#FFFFFF'
)
5.3 复合组件:HomeComp
文件位置 : entry/src/main/ets/pages/comp/HomeComp.ets
typescript
import TopNavComp from '../comp/TopNavComp'
import CardComp from '../comp/CardComp'
import { NavItem, navItems, lifeReminder, checkUp } from '../model/NavCard'
@Component
export default struct HomeComp {
// 获取全局路由栈
pathStack: NavPathStack = AppStorage.get("pathStack") as NavPathStack;
build() {
NavDestination() {
Column() {
// 1. 顶部导航栏
TopNavComp({ title: '泰克健康' })
Column() {
// 2. 功能网格
Grid() {
ForEach(navItems, (item: NavItem, index) => {
GridItem() {
CardComp({
wid: '50%',
params: item,
contentBuilder: navItemContent
})
}
.onClick(() => {
this.pathStack.pushPathByName(item.url, null)
})
})
}
.rowsTemplate('1fr 1fr') // 2行
.rowsGap(5)
.columnsGap('2%')
.height('24%')
// 3. 每日签到区域
Text('每日签到')
.fontSize(21)
.fontWeight(FontWeight.Bold)
Column() {
Stack({ alignContent: Alignment.Center }) {
// 背景层
Column()
.width('100%')
.height('100%')
.borderRadius(20)
.backgroundImage($r('app.media.bg'))
.backgroundImageSize(ImageSize.FILL)
// 内容层
Column() {
Text('每日签到,开启健康每一天')
.fontSize(18)
.fontColor('#ff3952a5')
// 星期签到
Row() {
Flex({ direction: FlexDirection.Row }) {
ForEach(['一', '二', '三', '四', '五', '六', '日'],
(item: string) => {
Column({ space: 10 }) {
Text(`周${item}`)
Image($r('app.media.icon_true_green'))
.width(27)
.aspectRatio(1)
}
.flexGrow(1)
})
}
}
.padding(10)
Button('点击签到')
.margin(10)
}
.width('100%')
.height('100%')
.padding(12)
.borderRadius(20)
.backgroundImage($r('app.media.home_meiriqiaodao2'))
// 装饰图片
Image($r('app.media.home_v'))
.width(75)
.height(95)
.position({ x: '75%', y: -55 })
}
}
.height('30%')
// 4. 生活提醒
Column() {
Text('生活提醒')
.fontSize(21)
.fontWeight(FontWeight.Bold)
CardComp({
gradientStart: '#FFEDE4',
gradientEnd: '#FFFFFF',
paddingValue: 5,
wid: '100%',
params: lifeReminder,
contentBuilder: itemContent
})
}
// 5. 体检预约
Column() {
Text('体检预约')
.fontSize(21)
.fontWeight(FontWeight.Bold)
CardComp({
gradientStart: '#CCF2FF',
gradientEnd: '#FFFFFF',
paddingValue: 5,
wid: '100%',
params: checkUp,
contentBuilder: itemContent
})
}
.onClick(() => {
this.pathStack.pushPathByName('HealthCheckPage', null)
})
}
.padding($r('app.float.global_padding_or_margin'))
}
.width('100%')
.height('100%')
.backgroundImage($r("app.media.bg"))
.backgroundImageSize(ImageSize.Cover)
}
}
}
// 导航卡片内容
@Builder
function navItemContent(param: NavItem) {
Row({ space: 2 }) {
Image(param.img).width(55).height(55)
Column({ space: 5 }) {
Text(param.title)
.fontSize(21)
.fontWeight(FontWeight.Bold)
Text(param.subtitle)
.fontSize(14)
.fontColor(Color.Gray)
}
.alignItems(HorizontalAlign.Start)
}
}
// 简单卡片内容
@Builder
function itemContent(param: NavItem) {
Row({ space: 2 }) {
Image(param.img).width(44).height(44)
Text(param.title).fontSize(14)
}
.width('100%')
}
知识点详解:
5.3.1 Grid 网格布局
typescript
Grid() {
ForEach(items, (item) => {
GridItem() {
// 网格项内容
}
})
}
.rowsTemplate('1fr 1fr') // 2行,平分高度
.columnsTemplate('1fr 1fr 1fr') // 3列,平分宽度
.rowsGap(10) // 行间距
.columnsGap(10) // 列间距
模板语法:
1fr
- 弹性单位(平分剩余空间)100px
- 固定像素1fr 2fr
- 按比例分配(1:2)
5.3.2 Flex 弹性布局
typescript
Flex({
direction: FlexDirection.Row, // 方向
justifyContent: FlexAlign.SpaceBetween, // 主轴对齐
alignItems: ItemAlign.Center // 交叉轴对齐
}) {
// 子组件
}
FlexDirection 枚举:
Row
- 水平(从左到右)RowReverse
- 水平(从右到左)Column
- 垂直(从上到下)ColumnReverse
- 垂直(从下到上)
5.3.3 flexGrow 属性
typescript
Column() {
// 内容
}
.flexGrow(1) // 占据剩余空间
5.3.4 position 绝对定位
typescript
Image($r('app.media.icon'))
.width(75)
.height(95)
.position({ x: '75%', y: -55 }) // 绝对定位
第六章:状态管理
6.1 组件内部状态:@State
typescript
@Component
struct Counter {
@State count: number = 0 // 状态变量
build() {
Column() {
Text(`计数: ${this.count}`)
Button('增加')
.onClick(() => {
this.count++ // 修改状态,自动刷新UI
})
}
}
}
@State 特点:
- ✅ 状态改变自动刷新UI
- ✅ 仅在当前组件内有效
- ✅ 支持基本类型和对象类型
6.2 父子组件通信:@Prop 和 @Link
6.2.1 @Prop - 单向传递
typescript
// 父组件
@Entry
@Component
struct Parent {
@State message: string = 'Hello'
build() {
Column() {
Child({ msg: this.message })
}
}
}
// 子组件
@Component
struct Child {
@Prop msg: string // 接收父组件数据
build() {
Text(this.msg)
}
}
@Prop 特点:
- ✅ 单向数据流:父 → 子
- ✅ 子组件修改不影响父组件
- ✅ 适合配置型数据
6.2.2 @Link - 双向绑定
typescript
// 父组件
@Entry
@Component
struct Parent {
@State count: number = 0
build() {
Column() {
Text(`父组件: ${this.count}`)
Child({ count: $count }) // 使用 $ 传递引用
}
}
}
// 子组件
@Component
struct Child {
@Link count: number // 双向绑定
build() {
Column() {
Text(`子组件: ${this.count}`)
Button('增加')
.onClick(() => {
this.count++ // 修改会同步到父组件
})
}
}
}
@Link 特点:
- ✅ 双向数据流:父 ↔ 子
- ✅ 子组件修改同步父组件
- ✅ 使用
$
传递引用
6.3 全局状态:AppStorage
typescript
// 设置全局状态
AppStorage.setOrCreate('CurrentTabIndex', 0)
AppStorage.setOrCreate('IsLoggedIn', false)
AppStorage.setOrCreate('pathStack', this.pathStack)
// 获取全局状态
let tabIndex = AppStorage.get<number>('CurrentTabIndex')
let pathStack = AppStorage.get("pathStack") as NavPathStack
// 使用 @StorageLink 装饰器
@Component
struct MyComponent {
@StorageLink('CurrentTabIndex') tabIndex: number = 0
build() {
Text(`当前标签: ${this.tabIndex}`)
}
}
AppStorage 使用场景:
- ✅ 跨页面共享数据
- ✅ 全局配置信息
- ✅ 用户登录状态
- ✅ 路由栈共享
6.4 状态管理最佳实践
typescript
// ✅ 推荐:合理使用状态
@State isLoading: boolean = false
@State userInfo: UserInfo | null = null
@State errorMsg: string = ''
// ❌ 避免:不必要的状态
// 可以通过计算得到的值不需要状态
@State fullName: string = '' // 不推荐
// ✅ 推荐:使用计算属性
get fullName(): string {
return this.firstName + ' ' + this.lastName
}
第七章:路由导航
7.1 router 路由跳转
7.1.1 基本用法
typescript
import { router } from '@kit.ArkUI'
// 1. 跳转到新页面(保留当前页面)
router.pushUrl({
url: 'pages/LoginPage'
})
// 2. 跳转并传递参数
router.pushUrl({
url: 'pages/DetailPage',
params: {
id: '123',
name: '张三'
}
})
// 3. 替换当前页面(不保留历史)
router.replaceUrl({
url: 'pages/HomePage'
})
// 4. 返回上一页
router.back()
// 5. 返回到指定页面
router.back({
url: 'pages/Index'
})
7.1.2 接收路由参数
typescript
import { router } from '@kit.ArkUI'
@Entry
@Component
struct DetailPage {
@State id: string = ''
@State name: string = ''
aboutToAppear() {
// 获取路由参数
const params = router.getParams() as Record<string, string>
this.id = params['id']
this.name = params['name']
}
build() {
Column() {
Text(`ID: ${this.id}`)
Text(`姓名: ${this.name}`)
}
}
}
7.2 Navigation 路由栈
7.2.1 配置路由
route_map.json
:
json
{
"routerMap": [
{
"name": "HealthCheckPage",
"pageSourceFile": "src/main/ets/pages/home/HealthCheckPage.ets",
"buildFunction": "HealthCheckPageBuilder"
},
{
"name": "HospitalRankingPage",
"pageSourceFile": "src/main/ets/pages/home/HospitalRankingPage.ets",
"buildFunction": "HospitalRankingPageBuilder"
}
]
}
7.2.2 创建路由栈
typescript
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack()
aboutToAppear(): void {
// 存储到全局,供其他组件使用
AppStorage.setOrCreate("pathStack", this.pathStack)
}
build() {
Navigation(this.pathStack) {
// 页面内容
}
}
}
7.2.3 使用路由栈跳转
typescript
@Component
export default struct HomeComp {
// 获取全局路由栈
pathStack: NavPathStack = AppStorage.get("pathStack") as NavPathStack
build() {
NavDestination() {
Column() {
Button('跳转到健康检测')
.onClick(() => {
// 跳转到命名路由
this.pathStack.pushPathByName('HealthCheckPage', null)
})
Button('跳转并传参')
.onClick(() => {
this.pathStack.pushPathByName('DetailPage', {
id: '123',
name: '测试'
})
})
Button('返回')
.onClick(() => {
this.pathStack.pop()
})
Button('清空路由栈')
.onClick(() => {
this.pathStack.clear()
})
}
}
}
}
7.2.4 目标页面配置
typescript
// 页面文件:HealthCheckPage.ets
@Builder
export function HealthCheckPageBuilder() {
HealthCheckPage()
}
@Component
struct HealthCheckPage {
// 接收路由参数
@State params: Record<string, Object> = {}
aboutToAppear() {
// 获取路由参数的方式
// this.params = ...
}
build() {
NavDestination() {
Column() {
Text('健康检测页面')
}
}
.title('健康检测') // 设置页面标题
}
}
7.3 路由对比
特性 | router | Navigation |
---|---|---|
使用场景 | 页面级跳转 | 组件级导航 |
是否需要配置 | 需要在 main_pages.json | 需要在 route_map.json |
传参方式 | params 对象 | pushPathByName 参数 |
历史管理 | 全局页面栈 | 组件内路由栈 |
适用场景 | 独立页面跳转 | 多层级导航 |
第八章:数据模型
8.1 定义数据模型
文件位置 : entry/src/main/ets/pages/model/NavCard.ets
typescript
// 导航卡片数据模型
class NavItem {
img: ResourceStr // 图片资源
title: string // 标题
subtitle?: string // 副标题(可选)
url?: string // 跳转路径(可选)
constructor(
img: ResourceStr,
title: string,
subtitle?: string,
url?: string
) {
this.img = img
this.title = title
this.subtitle = subtitle
this.url = url
}
}
// 创建数据实例
const navItems: NavItem[] = [
new NavItem(
$r('app.media.img_jiankangjiance'),
'健康检测',
'守护生活每一刻',
'HealthCheckPage'
),
new NavItem(
$r('app.media.img_yaopinshouce'),
'药品手册',
'明智用药',
''
),
new NavItem(
$r('app.media.img_yiyuanpaihang'),
'医院排行',
'选择优质资源',
'HospitalRankingPage'
)
]
// 单个数据实例
const lifeReminder: NavItem = new NavItem(
$r('app.media.home_life_reminder_svg'),
'今天需要吃降压药两粒哟'
)
// 导出供其他组件使用
export { NavItem, navItems, lifeReminder }
8.2 使用数据模型
typescript
import { NavItem, navItems } from '../model/NavCard'
@Component
export default struct HomeComp {
build() {
Column() {
// 使用数据模型
ForEach(navItems, (item: NavItem, index) => {
Row() {
Image(item.img)
Column() {
Text(item.title)
Text(item.subtitle)
}
}
})
}
}
}
8.3 接口 vs 类
typescript
// 方式1: 使用 interface(接口)
interface NavItemInterface {
img: ResourceStr
title: string
subtitle?: string
}
const item1: NavItemInterface = {
img: $r('app.media.icon'),
title: '标题'
}
// 方式2: 使用 class(类)
class NavItemClass {
img: ResourceStr
title: string
subtitle?: string
constructor(img: ResourceStr, title: string, subtitle?: string) {
this.img = img
this.title = title
this.subtitle = subtitle
}
// 可以添加方法
getFullTitle(): string {
return this.subtitle ? `${this.title} - ${this.subtitle}` : this.title
}
}
const item2 = new NavItemClass($r('app.media.icon'), '标题')
选择建议:
- ✅ 使用
interface
- 纯数据结构 - ✅ 使用
class
- 需要方法或构造函数
第九章:图表组件
9.1 集成第三方图表库
安装依赖:
bash
ohpm install @ohos/mpchart
oh-package.json5
:
json5
{
"dependencies": {
"@ohos/mpchart": "^2.0.0"
}
}
9.2 折线图实现
文件位置 : entry/src/main/ets/pages/view/LineCharts.ets
typescript
import {
JArrayList,
XAxis,
XAxisPosition,
YAxis,
YAxisLabelPosition,
LineDataSet,
LineData,
Mode,
LineChart,
LineChartModel,
LimitLine,
LimitLabelPosition,
EntryOhos,
IAxisValueFormatter
} from '@ohos/mpchart'
// 1. 自定义X轴格式化器
class WeekFormatter implements IAxisValueFormatter {
private readonly weeks = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
getFormattedValue(value: number): string {
return this.weeks[Math.round(value) % 7] || ''
}
}
@Component
export default struct LineCharts {
// 2. 初始化模型
private model: LineChartModel = new LineChartModel()
private dataSet: LineDataSet = new LineDataSet(new JArrayList<EntryOhos>(), "高压")
private dataSet2: LineDataSet = new LineDataSet(new JArrayList<EntryOhos>(), "低压")
// 阈值线
private limitLine1: LimitLine = new LimitLine(120, '危险阈值')
private limitLine2: LimitLine = new LimitLine(50, '警戒阈值')
// 3. 外部传入的数据
@Prop data1: Array<number | null> = [600, 200, 1000, 620, 790, 410, 175]
@Prop data2: Array<number | null> = [400, 150, 900, 200, 210, 300, 405]
@Prop wid: number | string = '300'
@Prop hei: number | string = '200'
aboutToAppear() {
// 4. 基础配置
this.model.getDescription()?.setEnabled(false) // 禁用描述
this.model.setDragEnabled(true) // 启用拖拽
// 5. 配置坐标轴
this.configureAxis()
// 6. 配置阈值线
this.configureLimitLines()
// 7. 绑定数据
this.model.setData(this.generateMockData())
this.model.setVisibleXRangeMaximum(7) // 显示7个点
}
// 8. 坐标轴配置
private configureAxis() {
// X轴配置
const xAxis = this.model.getXAxis()
xAxis?.setPosition(XAxisPosition.BOTTOM) // 位置:底部
xAxis?.setGranularity(1) // 最小间隔
xAxis?.setLabelCount(7, true) // 标签数量
xAxis?.setValueFormatter(new WeekFormatter()) // 自定义格式化
// 左Y轴配置
const leftAxis = this.model.getAxisLeft()
leftAxis?.setAxisMinimum(0) // 最小值
leftAxis?.setAxisMaximum(1000) // 最大值
leftAxis?.setPosition(YAxisLabelPosition.OUTSIDE_CHART)
leftAxis?.setSpaceTop(15) // 顶部留白
// 右Y轴禁用
this.model.getAxisRight()?.setEnabled(false)
}
// 9. 阈值线配置
private configureLimitLines() {
// 危险阈值线(红色虚线)
this.limitLine1.setLineWidth(3)
this.limitLine1.enableDashedLine(10, 10, 0) // 虚线样式
this.limitLine1.setTextSize(12)
this.limitLine1.setLabelPosition(LimitLabelPosition.RIGHT_TOP)
this.limitLine1.setLineColor("#FF3D71")
// 警戒阈值线(橙色虚线)
this.limitLine2.setLineWidth(3)
this.limitLine2.enableDashedLine(10, 10, 0)
this.limitLine2.setTextSize(12)
this.limitLine2.setLabelPosition(LimitLabelPosition.RIGHT_BOTTOM)
this.limitLine2.setLineColor("#FFAA33")
// 添加到Y轴
this.model.getAxisLeft()?.addLimitLine(this.limitLine1)
this.model.getAxisLeft()?.addLimitLine(this.limitLine2)
}
// 10. 生成数据
private generateMockData(): LineData {
// 第一条线数据
const values = new JArrayList<EntryOhos>()
for (let i = 0; i < this.data1.length; i++) {
values.add(new EntryOhos(i, this.data1[i]))
}
// 第二条线数据
const values2 = new JArrayList<EntryOhos>()
for (let i = 0; i < this.data2.length; i++) {
values2.add(new EntryOhos(i, this.data2[i]))
}
// 设置数据
this.dataSet.setValues(values)
this.dataSet2.setValues(values2)
// 数据集1样式
this.dataSet.setMode(Mode.CUBIC_BEZIER) // 贝塞尔曲线
this.dataSet.setColorByColor(Color.Blue) // 线条颜色
this.dataSet.setLineWidth(2) // 线宽
this.dataSet.setDrawCircles(false) // 不显示圆点
this.dataSet.setDrawFilled(true) // 填充渐变
// 数据集2样式
this.dataSet2.setColorByColor(Color.Green)
this.dataSet2.setDrawCircles(false)
this.dataSet2.setMode(Mode.CUBIC_BEZIER)
// 返回数据
return new LineData([this.dataSet, this.dataSet2])
}
// 11. 页面构建
build() {
Column() {
LineChart({ model: this.model })
.width(this.wid)
.height(this.hei)
.backgroundColor("#F5F7FA")
.onAppear(() => {
// 启动动画
this.model.animateXY(1500, 1500)
})
}
}
}
9.3 使用图表组件
typescript
import LineCharts from '../view/LineCharts'
@Component
struct DataPage {
@State bpHighData: number[] = [120, 125, 118, 130, 115, 122, 119]
@State bpLowData: number[] = [80, 85, 78, 90, 75, 82, 79]
build() {
Column() {
Text('血压趋势')
.fontSize(20)
.fontWeight(FontWeight.Bold)
LineCharts({
wid: '100%',
hei: 300,
data1: this.bpHighData,
data2: this.bpLowData
})
}
}
}
知识点总结:
- ✅ 使用第三方库需要先安装依赖
- ✅ 图表配置包括:坐标轴、样式、数据
- ✅ 可以自定义格式化器
- ✅ 支持多条数据线
第十章:最佳实践
10.1 组件设计原则
10.1.1 单一职责
typescript
// ❌ 不好:一个组件做太多事
@Component
struct BadComponent {
build() {
Column() {
// 导航栏
Row() { }
// 内容区
Column() { }
// 底部栏
Row() { }
}
}
}
// ✅ 好:拆分成多个组件
@Component
struct GoodComponent {
build() {
Column() {
TopNav()
ContentArea()
BottomBar()
}
}
}
10.1.2 可复用性
typescript
// ✅ 好:可配置的通用组件
@Component
export default struct CustomButton {
@Prop text: string
@Prop bgColor: string = '#4D91FF'
@Prop fontSize: number = 16
onClickHandler?: () => void
build() {
Button(this.text)
.backgroundColor(this.bgColor)
.fontSize(this.fontSize)
.onClick(() => {
this.onClickHandler?.()
})
}
}
10.2 性能优化
10.2.1 减少不必要的渲染
typescript
// ❌ 不好:频繁创建对象
build() {
Column() {
ForEach(this.items, (item) => {
Row() {
Text(item.name)
.fontSize(this.getSize()) // 每次都调用
}
})
}
}
// ✅ 好:缓存计算结果
private cachedSize: number = 16
aboutToAppear() {
this.cachedSize = this.getSize()
}
build() {
Column() {
ForEach(this.items, (item) => {
Row() {
Text(item.name)
.fontSize(this.cachedSize) // 使用缓存
}
})
}
}
10.2.2 LazyForEach 懒加载
typescript
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): Object {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if (pos >= 0) {
this.listeners.splice(pos, 1)
}
}
}
@Component
struct LongList {
private dataSource: BasicDataSource = new BasicDataSource()
build() {
List() {
LazyForEach(this.dataSource, (item: Object) => {
ListItem() {
Text(item.toString())
}
}, item => item.id)
}
}
}
10.3 代码组织
10.3.1 文件命名
✅ 推荐命名规范:
- 组件文件: PascalCase (HomeComp.ets, CardComp.ets)
- 页面文件: PascalCase (LoginPage.ets, Index.ets)
- 工具类: camelCase (httpUtil.ets, dateUtil.ets)
- 模型类: PascalCase (NavCard.ets, UserInfo.ets)
10.3.2 目录结构
pages/
├── comp/ # 公共组件
├── model/ # 数据模型
├── view/ # 视图组件
├── utils/ # 工具函数
├── home/ # 首页模块
├── profile/ # 个人中心模块
└── common/ # 通用样式
10.4 样式管理
10.4.1 使用 @Extend 统一样式
typescript
// 定义全局样式扩展
@Extend(Text)
function primaryText() {
.fontSize(16)
.fontColor('#333333')
.fontWeight(FontWeight.Normal)
}
@Extend(Text)
function titleText() {
.fontSize(24)
.fontColor('#000000')
.fontWeight(FontWeight.Bold)
}
// 使用
build() {
Column() {
Text('标题').titleText()
Text('内容').primaryText()
}
}
10.4.2 使用资源文件
resources/base/element/color.json
:
json
{
"color": [
{
"name": "primary_color",
"value": "#4D91FF"
},
{
"name": "text_primary",
"value": "#333333"
}
]
}
resources/base/element/float.json
:
json
{
"float": [
{
"name": "global_padding_or_margin",
"value": "16vp"
},
{
"name": "title_font_size",
"value": "24fp"
}
]
}
使用资源:
typescript
Text('标题')
.fontSize($r('app.float.title_font_size'))
.fontColor($r('app.color.text_primary'))
.padding($r('app.float.global_padding_or_margin'))
10.5 错误处理
typescript
// ✅ 好:完善的错误处理
private jumpToPage(url: string) {
try {
router.pushUrl({ url: url })
} catch (error) {
console.error(`路由跳转失败: ${error.code} - ${error.message}`)
// 显示错误提示
promptAction.showToast({
message: '页面跳转失败,请重试'
})
}
}
第十一章:常见问题
11.1 装饰器相关
Q1: @State 和 @Prop 的区别?
typescript
// @State: 组件内部状态
@State count: number = 0
// ✅ 修改会刷新UI
// ✅ 只在当前组件有效
// @Prop: 父组件传入的属性
@Prop count: number
// ✅ 父组件修改会同步到子组件
// ✅ 子组件修改不影响父组件
Q2: 什么时候使用 @Link?
typescript
// 需要父子组件双向同步时使用
@Component
struct Child {
@Link count: number // 双向绑定
build() {
Button(`子组件: ${this.count}`)
.onClick(() => {
this.count++ // 修改会同步到父组件
})
}
}
// 父组件传递时使用 $
Child({ count: $count })
11.2 布局相关
Q3: 如何实现水平居中?
typescript
// 方法1: Column + alignItems
Column() {
Text('居中文本')
}
.alignItems(HorizontalAlign.Center)
// 方法2: Row + justifyContent
Row() {
Text('居中文本')
}
.justifyContent(FlexAlign.Center)
Q4: 如何实现垂直居中?
typescript
// Column + justifyContent
Column() {
Text('居中文本')
}
.justifyContent(FlexAlign.Center)
.height('100%')
11.3 路由相关
Q5: router 和 Navigation 有什么区别?
特性 | router | Navigation |
---|---|---|
跳转范围 | 页面级 | 组件级 |
配置文件 | main_pages.json | route_map.json |
使用场景 | 独立页面 | 多层导航 |
返回栈 | 全局 | 组件内 |
Q6: 如何传递复杂对象?
typescript
// router 方式
router.pushUrl({
url: 'pages/DetailPage',
params: {
user: JSON.stringify({ // 转为JSON字符串
id: 1,
name: '张三'
})
}
})
// 接收时解析
const params = router.getParams()
const user = JSON.parse(params['user'])
11.4 性能相关
Q7: ForEach 和 LazyForEach 的区别?
typescript
// ForEach: 一次性渲染所有
ForEach(this.items, (item) => {
ListItem() { Text(item) }
})
// ✅ 适合少量数据(< 100)
// ❌ 大量数据会卡顿
// LazyForEach: 按需渲染
LazyForEach(this.dataSource, (item) => {
ListItem() { Text(item) }
})
// ✅ 适合大量数据
// ✅ 性能更好
11.5 样式相关
Q8: 如何设置圆角?
typescript
Column() { }
.borderRadius(12) // 所有角
.borderRadius({ // 单独设置
topLeft: 12,
topRight: 12,
bottomLeft: 0,
bottomRight: 0
})
Q9: 如何设置阴影?
typescript
Column() { }
.shadow({
radius: 10,
color: '#00000033',
offsetX: 0,
offsetY: 2
})
总结
学习路径建议
第1周: 熟悉项目结构,理解启动流程
↓
第2周: 学习组件开发,掌握装饰器
↓
第3周: 深入状态管理和路由导航
↓
第4周: 数据模型和图表组件
↓
第5周: 最佳实践和性能优化
实践建议
- 动手实践: 运行项目,修改代码,观察效果
- 阅读文档: 查阅 HarmonyOS 官方文档
- 调试技巧: 使用 console.log 和 DevEco 调试工具
- 代码对比: 对比本项目与官方示例
- 循序渐进: 从简单组件开始,逐步深入
进阶方向
- 🚀 网络请求和 HTTP 客户端
- 🗄️ 本地数据库(Preferences、RelationalStore)
- 📱 系统能力调用(相机、位置、通知)
- 🎨 动画和转场效果
- 🔐 安全和加密
- 📦 应用打包和发布
祝学习愉快!遇到问题随时查阅本文档。 🎉