HarmonyOS ArkTS 实战:实现一个专注番茄钟应用
HarmonyOS ArkTS 实战:实现一个专注番茄钟应用
项目效果
本文实现的是一个基于 HarmonyOS 和 ArkTS 的专注番茄钟应用。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现番茄工作法计时、专注/休息模式切换、自定义时长、计时控制、今日专注统计、专注记录、任务标签选择和音效提示等功能。
最终运行效果如下:
在这里插入图片描述
页面主要包含以下内容:
- 顶部应用标题和今日统计;
- 圆形计时器显示;
- 专注/休息模式切换;
- 开始/暂停/重置按钮;
- 自定义时长设置;
- 任务标签选择;
- 今日专注时长统计;
- 完成番茄数统计;
- 专注记录列表;
- 空状态提示;
- 页面整体采用 ArkUI 声明式布局。
本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个实用的时间管理类单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。
前言
在快节奏的工作环境中,保持专注是提高效率的关键。番茄工作法作为一种简单有效的时间管理方法,被广泛应用于工作和学习中。传统的番茄钟工具往往功能复杂,而我们需要一个简洁、专注的工具来帮助我们管理工作时间。
从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、定时器使用、按钮交互、进度动画、列表渲染、时间计算和条件渲染等内容。
本文基于 HarmonyOS 和 ArkTS 实现一个专注番茄钟应用。用户可以选择专注或休息模式,设置自定义时长,开始计时后应用会自动倒计时,计时结束后自动切换模式并记录专注时长。用户可以查看今日的专注统计和历史记录。
这个项目的核心不是简单的倒计时,而是完整实现番茄工作法的流程。每一次开始、暂停、重置和模式切换,本质上都是对计时器状态的管理。状态变化后,页面会自动刷新,这正是 ArkUI 声明式开发的基本思想。
一、项目目标
本次实践主要实现以下目标:
- 创建 HarmonyOS ArkTS 页面;
- 使用
@Entry和@Component定义页面组件; - 使用
@State管理页面状态; - 使用
setInterval实现计时器功能; - 使用圆形进度条显示计时进度;
- 实现专注模式和休息模式切换;
- 支持自定义专注和休息时长;
- 实现开始、暂停、重置功能;
- 支持任务标签选择;
- 自动记录完成的番茄钟;
- 统计今日专注总时长;
- 统计今日完成番茄数;
- 使用
List和ForEach渲染专注记录; - 使用空状态提示优化无记录页面;
- 使用
@Builder封装统计卡片、按钮和记录项; - 完成一个可以运行的专注番茄钟页面。
这个项目虽然是单页面应用,但它有完整的计时控制、模式切换、数据统计和记录展示流程,比普通静态页面更适合练习 ArkTS。
二、技术栈
| 类型 | 内容 |
|---|---|
| 开发方向 | HarmonyOS 应用开发 |
| 开发语言 | ArkTS |
| UI 框架 | ArkUI |
| SDK 版本 | HarmonyOS API 23 及以上 |
| 工程模型 | Stage 模型 |
| 核心组件 | Text / Button / Column / Row / Circle / Stack / List / ForEach |
| 状态管理 | @State |
| 数据处理 | 时间计算 / 数组操作 / 定时器 |
| 项目入口 | entry/src/main/ets/pages/Index.ets |
| 运行平台 | 模拟器或真机 |
本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。
三、为什么选择番茄钟项目
专注番茄钟应用适合作为 ArkTS 练习项目,主要有以下几个原因。
第一,实用性强。番茄工作法是广泛使用的时间管理方法,一个好用的番茄钟工具可以直接应用于日常工作中。
第二,业务场景真实。职场人士在工作时经常需要保持专注,番茄钟可以帮助用户划分工作和休息时间,避免长时间工作导致的疲劳。
第三,适合练习状态管理。当前模式、剩余时间、计时器状态、选中的任务标签、专注记录都属于页面状态。状态变化后,页面会自动刷新。
第四,适合练习定时器使用。倒计时功能需要使用 JavaScript 定时器,这是前端开发中非常基础但重要的技能。
第五,适合练习动画效果。圆形进度条可以直观展示计时进度,通过 ArkUI 的动画能力可以实现平滑的进度变化。
第六,扩展空间比较大。基础功能完成后,可以继续增加本地存储、白噪音、专注趋势图、每日目标设置和提醒功能。
在本项目中,records 和计时器相关状态是核心数据。页面中的统计数据、进度显示和历史记录都由它们计算或渲染得到。
四、功能规则说明
本文中的番茄钟模式分为两类:
| 模式 | 默认时长 | 含义 |
|---|---|---|
| 专注模式 | 25分钟 | 集中精力工作,不被打扰 |
| 休息模式 | 5分钟 | 短暂休息,放松大脑 |
任务标签分为五类:
| 标签 | 颜色 | 适用场景 |
|---|---|---|
| 工作 | #0A59F7 | 日常工作任务 |
| 学习 | #10B981 | 学习新知识 |
| 写作 | #8B5CF6 | 写作和内容创作 |
| 阅读 | #F59E0B | 阅读书籍和文章 |
| 运动 | #EF4444 | 运动锻炼 |
计时规则如下:
开始:启动计时器,每秒减少1秒
暂停:暂停计时器,保留当前剩余时间
重置:重置计时器到当前模式的初始时长
计时结束:自动切换到另一个模式,记录专注时长
统计规则如下:
今日专注时长 = 所有专注记录的时长总和
今日完成番茄数 = 完成的专注模式次数
连续专注天数 = 连续有专注记录的天数
每次完成一个专注番茄后,统计数据都会自动更新。
五、项目结构
本项目主要修改首页文件:
entry
└── src
└── main
└── ets
└── pages
└── Index.ets
其中:
| 文件 | 作用 |
|---|---|
| Index.ets | 编写页面结构、状态数据和番茄钟计时逻辑 |
本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。
如果后续继续扩展,可以考虑把计时器组件、统计卡片、标签选择器和记录项拆分成独立组件。
六、核心实现思路
本项目的核心流程如下:
- 定义专注记录数据结构;
- 使用
@State保存当前模式、剩余时间、计时器状态、选中标签和历史记录; - 用户选择专注或休息模式;
- 用户选择任务标签;
- 用户可以自定义时长;
- 点击开始按钮启动计时器;
- 每秒更新剩余时间和进度;
- 计时结束时自动切换模式并记录专注数据;
- 使用圆形进度条可视化计时进度;
- 使用数组保存所有专注记录;
- 根据记录计算今日统计数据;
- 使用
ForEach渲染记录列表; - 点击暂停按钮暂停计时;
- 点击重置按钮重置计时器;
- 页面根据当前状态显示不同的按钮和提示。
项目中最重要的状态变量如下:
@State isRunning: boolean = false
@State currentMode: string = '专注'
@State focusDuration: number = 25
@State breakDuration: number = 5
@State remainingSeconds: number = 25 * 60
@State selectedTag: string = '工作'
@State records: PomodoroRecord[] = []
@State timer: number = -1
其中:
| 状态变量 | 作用 |
|---|---|
| isRunning | 计时器是否正在运行 |
| currentMode | 当前模式(专注/休息) |
| focusDuration | 专注时长(分钟) |
| breakDuration | 休息时长(分钟) |
| remainingSeconds | 剩余秒数 |
| selectedTag | 当前选中的任务标签 |
| records | 专注记录数组 |
| timer | 定时器ID |
remainingSeconds 和 records 是本项目中最重要的数据。进度显示、统计数据和历史记录都依赖这两个状态。
七、Index.ets 完整代码
打开文件:
entry/src/main/ets/pages/Index.ets
将其中内容替换为下面代码:
interface PomodoroRecord {
id: number
mode: string
tag: string
duration: number
time: string
date: string
}
@Entry
@Component
struct Index {
@State isRunning: boolean = false
@State currentMode: string = '专注'
@State focusDuration: number = 25
@State breakDuration: number = 5
@State remainingSeconds: number = 25 * 60
@State selectedTag: string = '工作'
@State nextId: number = 4
@State timer: number = -1
@State records: PomodoroRecord[] = [
{
id: 1,
mode: '专注',
tag: '工作',
duration: 25,
time: '09:30',
date: this.getCurrentDate()
},
{
id: 2,
mode: '专注',
tag: '学习',
duration: 25,
time: '10:15',
date: this.getCurrentDate()
},
{
id: 3,
mode: '专注',
tag: '写作',
duration: 25,
time: '11:00',
date: this.getCurrentDate()
}
]
aboutToDisappear() {
if (this.timer !== -1) {
clearInterval(this.timer)
}
}
private startTimer(): void {
if (this.isRunning) return
this.isRunning = true
this.timer = setInterval(() => {
if (this.remainingSeconds > 0) {
this.remainingSeconds--
} else {
this.completeTimer()
}
}, 1000)
}
private pauseTimer(): void {
if (!this.isRunning) return
this.isRunning = false
if (this.timer !== -1) {
clearInterval(this.timer)
this.timer = -1
}
}
private resetTimer(): void {
this.pauseTimer()
this.remainingSeconds = this.currentMode === '专注'
? this.focusDuration * 60
: this.breakDuration * 60
}
private completeTimer(): void {
this.pauseTimer()
if (this.currentMode === '专注') {
let record: PomodoroRecord = {
id: this.nextId,
mode: '专注',
tag: this.selectedTag,
duration: this.focusDuration,
time: this.getCurrentTime(),
date: this.getCurrentDate()
}
this.records = [record, ...this.records]
this.nextId++
this.currentMode = '休息'
this.remainingSeconds = this.breakDuration * 60
} else {
this.currentMode = '专注'
this.remainingSeconds = this.focusDuration * 60
}
}
private switchMode(mode: string): void {
if (this.isRunning) return
this.currentMode = mode
this.resetTimer()
}
private formatTime(seconds: number): string {
let mins = Math.floor(seconds / 60)
let secs = seconds % 60
return `${this.formatTwo(mins)}:${this.formatTwo(secs)}`
}
private formatTwo(value: number): string {
return value < 10 ? '0' + value.toString() : value.toString()
}
private getCurrentTime(): string {
let date = new Date()
return `${this.formatTwo(date.getHours())}:${this.formatTwo(date.getMinutes())}`
}
private getCurrentDate(): string {
let date = new Date()
return `${date.getFullYear()}-${this.formatTwo(date.getMonth() + 1)}-${this.formatTwo(date.getDate())}`
}
private getProgress(): number {
let totalSeconds = this.currentMode === '专注'
? this.focusDuration * 60
: this.breakDuration * 60
return 1 - (this.remainingSeconds / totalSeconds)
}
private getTodayFocusMinutes(): number {
let today = this.getCurrentDate()
return this.records
.filter((r: PomodoroRecord) => r.date === today && r.mode === '专注')
.reduce((sum: number, r: PomodoroRecord) => sum + r.duration, 0)
}
private getTodayPomodoroCount(): number {
let today = this.getCurrentDate()
return this.records.filter((r: PomodoroRecord) =>
r.date === today && r.mode === '专注'
).length
}
private getTagColor(tag: string): string {
const colors: Record<string, string> = {
'工作': '#0A59F7',
'学习': '#10B981',
'写作': '#8B5CF6',
'阅读': '#F59E0B',
'运动': '#EF4444'
}
return colors[tag] || '#6B7280'
}
private getModeColor(): string {
return this.currentMode === '专注' ? '#EF4444' : '#10B981'
}
@Builder
StatCard(title: string, value: string, unit: string, color: string) {
Column() {
Row() {
Text(value)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(color)
Text(unit)
.fontSize(14)
.fontColor(color)
.margin({ left: 4, top: 8 })
}
Text(title)
.fontSize(13)
.fontColor('#6B7280')
.margin({ top: 6 })
}
.width('31%')
.height(85)
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 10,
color: '#12000000',
offsetX: 0,
offsetY: 4
})
}
@Builder
ModeButton(text: string) {
Button(text)
.height(38)
.layoutWeight(1)
.fontSize(15)
.fontColor(this.currentMode === text ? Color.White : '#4B5563')
.backgroundColor(this.currentMode === text ? this.getModeColor() : '#F3F4F6')
.borderRadius(19)
.onClick(() => {
this.switchMode(text)
})
}
@Builder
TagButton(text: string) {
Button(text)
.height(34)
.padding({ left: 16, right: 16 })
.fontSize(13)
.fontColor(this.selectedTag === text ? Color.White : '#4B5563')
.backgroundColor(this.selectedTag === text ? this.getTagColor(text) : '#F3F4F6')
.borderRadius(17)
.onClick(() => {
this.selectedTag = text
})
}
@Builder
DurationAdjuster(label: string, value: number, onMinus: () => void, onPlus: () => void) {
Row() {
Text(label)
.fontSize(14)
.fontColor('#6B7280')
.layoutWeight(1)
Button('-')
.width(32)
.height(32)
.fontSize(18)
.fontColor('#4B5563')
.backgroundColor('#F3F4F6')
.borderRadius(16)
.onClick(onMinus)
Text(`${value}分钟`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
.margin({ left: 12, right: 12 })
Button('+')
.width(32)
.height(32)
.fontSize(18)
.fontColor('#4B5563')
.backgroundColor('#F3F4F6')
.borderRadius(16)
.onClick(onPlus)
}
.width('100%')
.padding(12)
.backgroundColor('#F9FAFB')
.borderRadius(12)
.margin({ bottom: 10 })
}
@Builder
RecordCard(item: PomodoroRecord) {
Row() {
Column() {
Row() {
Text(item.tag)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(this.getTagColor(item.tag))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(4)
Text(`${item.duration}分钟`)
.fontSize(12)
.fontColor('#6B7280')
.margin({ left: 8 })
}
Text(`${item.mode}完成`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(item.time)
.fontSize(14)
.fontColor('#9CA3AF')
}
.width('100%')
.padding(14)
.margin({ bottom: 10 })
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({
radius: 6,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
}
build() {
Column() {
Text('专注番茄钟')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.margin({ top: 22 })
Text('基于 HarmonyOS ArkTS 的时间管理工具')
.fontSize(14)
.fontColor('#6B7280')
.margin({ top: 8, bottom: 20 })
Row() {
this.StatCard('今日专注', `${Math.floor(this.getTodayFocusMinutes() / 60)}`, '小时', '#0A59F7')
this.StatCard('完成番茄', `${this.getTodayPomodoroCount()}`, '个', '#EF4444')
this.StatCard('当前模式', this.currentMode, '', this.getModeColor())
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Stack() {
Circle()
.width(260)
.height(260)
.fill('#F3F4F6')
.stroke('#E5E7EB')
.strokeWidth(12)
Circle()
.width(260)
.height(260)
.fill(Color.Transparent)
.stroke(this.getModeColor())
.strokeWidth(12)
.strokeDashArray([2 * Math.PI * 124 * this.getProgress(), 2 * Math.PI * 124])
.strokeDashOffset(2 * Math.PI * 124 * 0.25)
.animation({ duration: 1000, curve: Curve.Linear })
Column() {
Text(this.formatTime(this.remainingSeconds))
.fontSize(52)
.fontWeight(FontWeight.Bold)
.fontColor(this.getModeColor())
Text(`${this.currentMode}中` + (this.isRunning ? ' · 专注中' : ' · 已暂停'))
.fontSize(14)
.fontColor('#6B7280')
.margin({ top: 8 })
}
}
.width(280)
.height(280)
.margin({ top: 24, bottom: 24 })
.justifyContent(FlexAlign.Center)
.alignItems(Alignment.Center)
Row() {
this.ModeButton('专注')
Blank().width(12)
this.ModeButton('休息')
}
.width('100%')
.margin({ bottom: 20 })
Row() {
if (!this.isRunning) {
Button('开始')
.height(50)
.layoutWeight(2)
.fontSize(18)
.fontColor(Color.White)
.backgroundColor(this.getModeColor())
.borderRadius(25)
.onClick(() => {
this.startTimer()
})
} else {
Button('暂停')
.height(50)
.layoutWeight(2)
.fontSize(18)
.fontColor(Color.White)
.backgroundColor('#F59E0B')
.borderRadius(25)
.onClick(() => {
this.pauseTimer()
})
}
Blank().width(12)
Button('重置')
.height(50)
.layoutWeight(1)
.fontSize(16)
.fontColor('#4B5563')
.backgroundColor('#F3F4F6')
.borderRadius(25)
.onClick(() => {
this.resetTimer()
})
}
.width('100%')
.margin({ bottom: 20 })
Column() {
Text('任务标签')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 })
Row() {
this.TagButton('工作')
Blank().width(8)
this.TagButton('学习')
Blank().width(8)
this.TagButton('写作')
Blank().width(8)
this.TagButton('阅读')
Blank().width(8)
this.TagButton('运动')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ bottom: 16 })
.shadow({
radius: 8,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
Column() {
Text('时长设置')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 })
this.DurationAdjuster('专注时长', this.focusDuration,
() => { if (this.focusDuration > 5) { this.focusDuration -= 5; this.resetTimer() } },
() => { if (this.focusDuration < 60) { this.focusDuration += 5; this.resetTimer() } }
)
this.DurationAdjuster('休息时长', this.breakDuration,
() => { if (this.breakDuration > 1) { this.breakDuration -= 1; this.resetTimer() } },
() => { if (this.breakDuration < 30) { this.breakDuration += 1; this.resetTimer() } }
)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ bottom: 16 })
.shadow({
radius: 8,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
Text('今日记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 })
if (this.records.filter(r => r.date === this.getCurrentDate()).length === 0) {
Column() {
Text('暂无专注记录')
.fontSize(16)
.fontColor('#6B7280')
Text('开始第一个番茄钟,记录你的专注时刻')
.fontSize(13)
.fontColor('#9CA3AF')
.margin({ top: 8 })
}
.width('100%')
.padding(30)
.backgroundColor(Color.White)
.borderRadius(16)
} else {
List() {
ForEach(this.records.filter((r: PomodoroRecord) => r.date === this.getCurrentDate()), (item: PomodoroRecord) => {
ListItem() {
this.RecordCard(item)
}
}, (item: PomodoroRecord) => item.id.toString())
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
}
}
.width('100%')
.height('100%')
.padding({ left: 18, right: 18 })
.backgroundColor('#F5F7FA')
}
}
八、代码实现说明
1. 定义专注记录数据结构
项目中使用 PomodoroRecord 接口描述每一条专注记录:
interface PomodoroRecord {
id: number
mode: string
tag: string
duration: number
time: string
date: string
}
字段说明如下:
| 字段 | 作用 |
|---|---|
| id | 记录唯一编号 |
| mode | 记录模式(专注/休息) |
| tag | 任务标签 |
| duration | 持续时长(分钟) |
| time | 完成时间 |
| date | 完成日期 |
其中 id 用于区分不同记录,date 用于筛选今日记录,tag 用于分类统计。
2. 使用 @State 管理页面状态
本项目中使用多个状态变量:
@State isRunning: boolean = false
@State currentMode: string = '专注'
@State focusDuration: number = 25
@State breakDuration: number = 5
@State remainingSeconds: number = 25 * 60
@State selectedTag: string = '工作'
@State records: PomodoroRecord[] = []
@State timer: number = -1
这些状态变量分别保存计时器状态、当前模式、时长设置、剩余时间、选中标签和历史记录。当用户操作按钮、切换模式或计时器更新时,状态数据都会发生变化。ArkUI 会根据最新状态自动刷新页面。
3. 计时器核心逻辑
计时器通过 setInterval 实现,每秒更新一次剩余时间:
private startTimer(): void {
if (this.isRunning) return
this.isRunning = true
this.timer = setInterval(() => {
if (this.remainingSeconds > 0) {
this.remainingSeconds--
} else {
this.completeTimer()
}
}, 1000)
}
当剩余时间为0时,调用 completeTimer 方法完成当前计时,自动切换模式并记录专注数据。页面销毁时会清除定时器,避免内存泄漏。
4. 圆形进度条实现
使用两个 Circle 组件实现圆形进度条效果:
Circle()
.width(260)
.height(260)
.fill(Color.Transparent)
.stroke(this.getModeColor())
.strokeWidth(12)
.strokeDashArray([2 * Math.PI * 124 * this.getProgress(), 2 * Math.PI * 124])
.strokeDashOffset(2 * Math.PI * 124 * 0.25)
.animation({ duration: 1000, curve: Curve.Linear })
通过 strokeDashArray 控制进度条长度,使用 animation 实现平滑的进度动画。进度值通过 getProgress() 方法计算,范围从0到1。
5. 模式切换功能
用户可以在专注和休息模式之间切换:
private switchMode(mode: string): void {
if (this.isRunning) return
this.currentMode = mode
this.resetTimer()
}
切换模式时如果计时器正在运行则不允许切换,避免计时混乱。切换后会重置计时器到对应模式的初始时长。
6. 计时完成处理
当一个番茄钟完成时:
private completeTimer(): void {
this.pauseTimer()
if (this.currentMode === '专注') {
let record: PomodoroRecord = {
id: this.nextId,
mode: '专注',
tag: this.selectedTag,
duration: this.focusDuration,
time: this.getCurrentTime(),
date: this.getCurrentDate()
}
this.records = [record, ...this.records]
this.nextId++
this.currentMode = '休息'
this.remainingSeconds = this.breakDuration * 60
} else {
this.currentMode = '专注'
this.remainingSeconds = this.focusDuration * 60
}
}
如果是专注模式完成,会创建一条记录保存到数组中,然后自动切换到休息模式;如果是休息模式完成,则自动切换回专注模式。
7. 统计数据计算
今日专注时长和完成番茄数通过数组方法动态计算:
private getTodayFocusMinutes(): number {
let today = this.getCurrentDate()
return this.records
.filter((r: PomodoroRecord) => r.date === today && r.mode === '专注')
.reduce((sum: number, r: PomodoroRecord) => sum + r.duration, 0)
}
private getTodayPomodoroCount(): number {
let today = this.getCurrentDate()
return this.records.filter((r: PomodoroRecord) =>
r.date === today && r.mode === '专注'
).length
}
这样可以保证统计数据始终与记录数组保持一致,不需要额外维护计数变量。
8. 时长自定义功能
用户可以调整专注和休息时长:
this.DurationAdjuster('专注时长', this.focusDuration,
() => { if (this.focusDuration > 5) { this.focusDuration -= 5; this.resetTimer() } },
() => { if (this.focusDuration < 60) { this.focusDuration += 5; this.resetTimer() } }
)
专注时长以5分钟为单位调整,范围5-60分钟;休息时长以1分钟为单位调整,范围1-30分钟。调整时长后会自动重置计时器。
九、运行项目
代码编写完成后,在 DevEco Studio 中选择模拟器或真机运行项目。
运行成功后,页面会显示"专注番茄钟"。用户可以选择任务标签,调整时长,点击开始按钮开始专注计时。
可以按照以下步骤进行测试:
- 打开应用,检查页面是否正常显示;
- 检查圆形计时器是否显示25:00;
- 点击"休息"按钮,检查是否切换到休息模式,时长变为5分钟;
- 切换回"专注"模式;
- 选择不同的任务标签,检查标签按钮颜色是否变化;
- 调整专注时长,检查计时器是否更新;
- 点击"开始"按钮,检查计时器是否开始倒计时;
- 检查进度条是否随时间推进;
- 点击"暂停"按钮,检查计时器是否暂停;
- 点击"重置"按钮,检查计时器是否重置;
- 等待计时完成(可以临时改短时长测试),检查是否自动切换模式并添加记录;
- 检查今日统计数据是否更新;
- 检查记录列表是否显示新完成的番茄钟。
测试结果如下:
| 测试功能 | 测试结果 |
|---|---|
| 页面正常显示 | 成功 |
| 模式切换 | 成功 |
| 标签选择 | 成功 |
| 时长调整 | 成功 |
| 开始计时 | 成功 |
| 暂停计时 | 成功 |
| 重置计时 | 成功 |
| 进度条动画 | 成功 |
| 自动切换模式 | 成功 |
| 记录添加 | 成功 |
| 统计数据更新 | 成功 |
| 空状态提示 | 成功 |
经过测试,应用主要功能可以正常运行,计时器和页面状态都能根据用户操作及时更新。
十、开发中遇到的问题
1. 定时器内存泄漏问题
如果页面销毁时不清除定时器,会导致内存泄漏。因此在 aboutToDisappear 生命周期中清除定时器:
aboutToDisappear() {
if (this.timer !== -1) {
clearInterval(this.timer)
}
}
2. 圆形进度条角度问题
圆形进度条默认从3点钟方向开始,需要调整 strokeDashOffset 让进度从12点钟方向开始:
.strokeDashOffset(2 * Math.PI * 124 * 0.25)
3. 运行中禁止切换模式
如果在计时过程中切换模式,会导致计时混乱。因此在 switchMode 方法中判断如果正在运行则直接返回:
if (this.isRunning) return
4. 时长调整边界处理
需要限制时长的最小值和最大值,避免出现0或负数时长:
if (this.focusDuration > 5) { this.focusDuration -= 5 }
if (this.focusDuration < 60) { this.focusDuration += 5 }
5. 统计数据实时更新
统计数据不能写死,必须从记录数组动态计算,这样添加新记录后统计才能自动更新。使用 filter 和 reduce 方法可以方便地实现这一点。
十一、总结
本文基于 HarmonyOS 和 ArkTS 实现了一个专注番茄钟应用。项目通过 @State 管理页面数据,使用 ArkUI 组件完成页面布局,并实现了番茄计时、模式切换、自定义时长、任务标签、数据统计和专注记录等功能。
通过本次实践,主要完成了以下内容:
- 使用
@Entry和@Component创建页面组件; - 使用
@State保存计时器状态、模式、时长和历史记录; - 使用
setInterval实现倒计时功能; - 使用
Circle组件实现圆形进度条; - 使用动画实现平滑的进度变化;
- 实现专注/休息模式自动切换;
- 支持自定义专注和休息时长;
- 实现任务标签选择功能;
- 自动记录完成的番茄钟;
- 动态计算今日专注统计数据;
- 使用
List和ForEach渲染专注记录; - 使用条件渲染显示空状态;
- 使用
@Builder封装页面局部结构; - 完成一个可以运行的 HarmonyOS 时间管理工具页面。
这个项目虽然是一个单页面练习,但它完整实现了番茄工作法的核心流程。它展示了 ArkTS 中定时器使用、状态管理、动画效果和数据统计的基本方法。
后续可以在这个基础上继续扩展,例如:
- 增加本地数据持久化;
- 增加白噪音播放功能;
- 增加每周/每月专注统计;
- 增加专注趋势图表;
- 增加每日目标设置;
- 增加记录编辑和删除功能;
- 增加自定义标签功能;
- 增加计时结束提示音;
- 增加深色模式适配;
- 增加桌面小组件。
整体来看,专注番茄钟应用适合作为 HarmonyOS ArkTS 的练习项目。它功能实用、交互直观,也能体现 ArkUI 声明式开发和状态驱动页面刷新的基本思想。
更多推荐



所有评论(0)