BLOG_番茄钟开发实战
鸿蒙Next番茄钟应用开发实战:从零构建高效专注力工具
一、前言
番茄工作法(Pomodoro Technique)是由 Francesco Cirillo 在 20 世纪 80 年代末发明的一种时间管理方法。其核心思想是将工作时间分割成 25 分钟专注工作 + 5 分钟短暂休息的循环周期,每完成 4 个周期后进行一次较长休息。这种工作方式之所以有效,是因为它利用了人类大脑的注意规律——大多数人无法保持超过 25 分钟的极度专注,而高频率的短暂休息能让大脑维持最佳工作状态。
在移动互联网时代,番茄钟应用层出不穷,从 iOS 上的 Forest、Focus Timer,到 Android 上的 Tide、番茄ToDo,再到各种桌面端应用,番茄工作法的载体不断进化。然而在 HarmonyOS Next 生态中,这类应用仍然相对稀缺。作为一名鸿蒙开发者,我决定用 ArkTS 语言在 HarmonyOS Next 平台上开发一款原生的番茄钟应用,既锻炼自己的鸿蒙开发能力,也为生态贡献一份力量。
本文将从需求分析、技术选型、架构设计、UI 实现、计时逻辑、状态管理等方面,详细记录这款番茄钟应用的完整开发过程,希望能为其他鸿蒙开发者提供参考。
二、需求分析与技术选型
2.1 核心功能需求
在动手编码之前,我首先梳理了番茄钟应用的核心功能需求:
| 功能 | 描述 | 优先级 |
|---|---|---|
| 专注计时 | 25 分钟倒计时,结束时自动切换 | P0 |
| 休息计时 | 5 分钟倒计时,结束时自动切换 | P0 |
| 循环计数 | 记录已完成的工作周期数,默认 4 轮 | P0 |
| 开始/暂停 | 用户可以随时开始或暂停计时 | P0 |
| 重置 | 一键回到初始工作状态 | P1 |
| 跳过休息 | 休息阶段可跳过直接进入下一轮工作 | P1 |
| 进度可视化 | 环形进度条直观显示时间流逝 | P1 |
| 模式切换提示 | 清晰标识当前是"工作"还是"休息"模式 | P1 |
| 背景与视觉反馈 | 不同颜色区分布局,沉浸式体验 | P2 |
2.2 技术选型
| 技术层面 | 选择 | 原因 |
|---|---|---|
| 开发语言 | ArkTS | HarmonyOS Next 原生声明式语言 |
| UI 框架 | ArkUI | 鸿蒙原生声明式 UI 框架 |
| 状态管理 | @State + 局部状态 | 轻量级应用,无需引入复杂状态管理 |
| 计时方案 | setInterval | 简单可靠,秒级精度足够 |
| 构建工具 | Hvigor | 鸿蒙官方构建工具 |
| 最低 API | 24 | 覆盖绝大多数 HarmonyOS Next 设备 |
2.3 为什么选择 ArkTS 而非 Java/JS?
HarmonyOS Next 全面拥抱 ArkTS 作为首选手语言。相比传统的 Java 或 JavaScript 方案,ArkTS 提供了:
- 类型安全:静态类型检查在编译期捕获大量错误
- 声明式 UI:类似 SwiftUI / Jetpack Compose 的 UI 描述方式,代码更简洁
- 高性能运行时:ArkCompiler 直接编译为机器码,无 JVM 开销
- 和前端开发者的亲和力:语法与 TypeScript 高度一致,学习曲线平缓
实际体验下来,ArkTS + ArkUI 的组合确实带来了高效的开发体验,尤其是在 UI 构建和状态管理方面,比起传统的 XML + Activity 方案要简洁得多。
三、项目初始化与工程结构
3.1 创建项目
使用 DevEco Studio 创建 HarmonyOS Next 工程时,我选择了以下配置:
- 模板:Empty Ability (Stage Model)
- 语言:ArkTS
- 最低 API:API 24 (HarmonyOS 7.0)
- 编译 SDK:API 24
Stage Model 是 HarmonyOS Next 推荐的应用开发模型,相比于 FA (Feature Ability) 模型,Stage 模型在组件化、生命周期管理、跨设备迁移等方面有显著优势。
3.2 项目目录结构
Pomotodo/
├── AppScope/ # 应用全局配置
│ └── resources/
│ ├── base/
│ │ ├── element/
│ │ │ └── string.json # 应用名称等字符串
│ │ └── media/ # 应用图标
│ └── ...
├── entry/ # 主模块
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # Ability 入口
│ │ │ └── pages/
│ │ │ └── Index.ets # 主页面(番茄钟主界面)
│ │ ├── resources/
│ │ │ ├── base/
│ │ │ │ ├── element/
│ │ │ │ │ ├── string.json # 应用内字符串
│ │ │ │ │ ├── color.json # 颜色定义
│ │ │ │ │ └── float.json # 尺寸定义
│ │ │ │ └── profile/
│ │ │ │ └── main_pages.json # 页面路由
│ │ │ └── ...
│ │ └── resources/profile/
│ └── build-profile.json5 # 模块构建配置
├── build-profile.json5 # 项目构建配置(含 API 版本)
└── hvigor/ # 构建脚本
这个结构的优点是:
- 模块化清晰:entry 作为主模块,后续可以扩展其他模块(如 watch 版、平板版)
- 资源配置分离:字符串、颜色、尺寸等资源与代码分离,方便多语言适配
- 构建配置集中:API 版本、签名配置等集中在 build-profile.json5 中管理
3.3 配置 API 24
在 build-profile.json5 中,我配置了目标 SDK 和兼容 SDK 均为 API 24:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "7.0.0(24)",
"compatibleSdkVersion": "7.0.0(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
API 24 覆盖了 HarmonyOS 7.0 及以上的设备,确保了应用的广泛兼容性。同时,应用也利用了 API 24 中新增的 Progress 组件增强特性,比如 Ring 类型的环形进度条。
四、UI 设计与实现
4.1 设计思路
番茄钟应用的目标是帮助用户专注,因此 UI 设计遵循以下原则:
- 极简主义:界面元素精简到最少,避免视觉干扰
- 信息层级清晰:倒计时 > 模式状态 > 控制按钮 > 辅助信息
- 色彩区分模式:工作模式用红色/暖色,休息模式用绿色/冷色
- 沉浸体验:深色渐变背景减少视觉疲劳
4.2 整体布局
UI 层采用 Stack + Column 的嵌套结构:
Stack { // 最外层,用于层叠背景和内容
Column { // 背景渐变层
linearGradient(...)
}
Column { // 内容层,垂直居中排列
Text(标题) // "番茄钟 🍅"
Text(轮次) // "第 1 / 4 轮"
Text(模式标签) // "WORK" / "BREAK"
Text(状态文字) // "专注工作" / "休息一下 ☕"
Stack { // 圆环 + 倒计时
Progress(Ring) // 环形进度条
Column {
Text(MM:SS) // 倒计时数字
Text(运行状态)
}
}
Row { // 控制按钮行
Button(重置)
Button(开始/暂停)
Button(跳过)
}
Text(底部提示) // "25分钟专注工作"
}
}
4.3 深色渐变背景
我使用了三层渐变颜色来营造沉浸感:
Column()
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#1a1a2e', 0.0], ['#16213e', 0.5], ['#0f3460', 1.0]]
})
从深紫灰 (#1a1a2e) 过渡到深蓝 (#0f3460),视觉效果沉稳而不沉闷。这种配色方案在番茄钟应用中很常见,因为它能降低视觉兴奋度,帮助用户保持专注。
4.4 标题与状态显示
标题使用了醒目的红色 (#e94560) 来吸引注意力,同时通过 FontWeight.Bold 增强视觉重量:
Text('番茄钟 🍅')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#e94560')
模式标签(WORK / BREAK)采用带边框的胶囊样式设计,颜色随模式切换而变化:
Text(this.phaseText)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(this.isWork ? '#e94560' : '#4ecca3')
.border({
width: 1,
color: this.isWork ? '#e94560' : '#4ecca3',
style: BorderStyle.Solid
})
.borderRadius(12)
这里使用了 #e94560(番茄红)代表工作模式,`#4ecca3(青绿色)代表休息模式,通过颜色对比让用户一眼就能识别当前状态。
4.5 环形进度条
环形进度条是番茄钟的视觉核心。在 HarmonyOS ArkUI 中,Progress 组件提供了 ProgressType.Ring 类型来渲染环形进度:
Progress({
value: this.progress * 100,
total: 100,
type: ProgressType.Ring
})
.width(260)
.height(260)
.style({ strokeWidth: 12 })
.color(this.isWork ? '#e94560' : '#4ecca3')
.backgroundColor('rgba(255,255,255,0.08)')
关键点说明:
value / total:进度值范围,我使用百分比制(0~100)strokeWidth: 12:环的线宽为 12vp,视觉效果饱满.color():动态切换环的颜色,红色代表工作,绿色代表休息.backgroundColor():底环使用半透明白色,形成层次感
Progress 组件会自动处理从 0 到 100 的平滑过渡动画,无需额外编写动画代码。这是声明式 UI 框架的优势——框架层自动处理了属性变化到视觉表现的映射。
4.6 倒计时数字
倒计时数字采用等宽字体(Courier New),确保数字变化时显示宽度不变,维持视觉稳定性:
Text(this.formatTime(this.remainSec))
.fontSize(56)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
.fontFamily('Courier New')
格式化函数将秒数转换为 MM:SS 格式,padStart(2, '0') 确保个位数前补零:
formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
这种显示方式的优点是:
25:00格式比单纯显示数字1500更直观- 等宽字体保证
25:00和00:01宽度一致 - 大字号(56fp)从远处也能清晰阅读
4.7 控制按钮
三个按钮采用不同的视觉权重来表明功能层次:
| 按钮 | 视觉权重 | 交互逻辑 |
|---|---|---|
| ⏹ 重置 | 低(半透明背景) | 回到初始工作状态 |
| 开始/暂停 | 高(实色背景 + 阴影) | 切换计时运行状态 |
| ⏭ 跳过 | 低(半透明背景,休息时可用) | 跳过当前休息阶段 |
主按钮通过 @State isRunning 动态切换颜色和文字:
Button(this.buttonText)
.backgroundColor(this.isRunning ? '#e94560' : '#4ecca3')
.shadow({
radius: 12,
color: this.isRunning ? 'rgba(233,69,96,0.4)' : 'rgba(78,204,163,0.4)'
})
.onClick(() => { this.startTimer(); })
运行时按钮为红色,暂停时按钮为绿色,配合阴影效果增强了按钮的立体感和可点击性。
跳过按钮通过 .enabled(!this.isWork) 实现在工作模式下不可用,防止误操作:
Button('⏭ 跳过')
.enabled(!this.isWork)
.onClick(() => {
if (!this.isWork) {
this.stopTimer();
this.switchMode();
}
})
五、计时逻辑与状态管理
5.1 状态变量设计
ArkTS 中使用 @State 装饰器标记响应式状态变量。我设计了 5 个核心状态:
@State remainSec: number = 25 * 60; // 剩余秒数
@State isWork: boolean = true; // 工作模式 OR 休息模式
@State isRunning: boolean = false; // 计时器运行中 OR 已暂停
@State cycleCount: number = 1; // 当前轮次(1~4)
@State progress: number = 1.0; // 进度百分比(0~1)
此外,还有 4 个辅助显示文本:
@State statusText: string = '专注工作'; // 状态提示
@State buttonText: string = '开始'; // 按钮文字
@State cycleText: string = '第 1 / 4 轮'; // 轮次信息
@State phaseText: string = 'WORK'; // 模式标签
这些状态变量的设计遵循"最小化原则"——只存储真正需要响应的数据,派生数据(如格式化的时间字符串)在渲染时计算。
5.2 计时器实现
计时器使用 setInterval 实现每秒触发一次 onTick 方法:
startTimer(): void {
if (this.isRunning) {
// 暂停
this.stopTimer();
this.buttonText = '继续';
this.isRunning = false;
} else {
// 开始/继续
this.isRunning = true;
this.buttonText = '暂停';
this.timerId = setInterval(() => {
this.onTick();
}, 1000);
}
}
这种"同一个按钮处理开始和暂停"的设计,符合用户对计时器的直觉预期——点击一次开始,再点击一次暂停,不需要寻找两个不同的按钮。
stopTimer 方法通过 clearInterval 停止计时,同时将 timerId 重置为 -1:
stopTimer(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.isRunning = false;
}
这里的关键是 timerId !== -1 的检查,避免了在计时器未启动时误调用 clearInterval。
5.3 每秒更新逻辑
onTick 方法是计时核心,每秒执行一次:
onTick(): void {
this.remainSec--;
if (this.remainSec < 0) {
this.remainSec = 0;
}
this.progress = this.remainSec / this.totalSec;
if (this.remainSec <= 0) {
this.stopTimer();
this.switchMode();
}
}
逻辑流程:
- 剩余秒数减 1
- 防止负数(边界保护)
- 计算进度百分比
- 如果倒计时归零,停止计时器,切换模式
这里 this.progress 的变化会自动触发 ArkUI 重新渲染 Progress 组件,因为 @State 变量是响应式的。
5.4 工作/休息模式切换
switchMode 方法处理模式切换这个核心业务逻辑:
switchMode(): void {
if (this.isWork) {
// 工作结束 -> 开始休息
this.isWork = false;
this.totalSec = this.BREAK_TIME;
this.remainSec = this.BREAK_TIME;
this.progress = 1.0;
this.statusText = '休息一下 ☕';
this.phaseText = 'BREAK';
this.buttonText = '开始';
} else {
// 休息结束 -> 开始下一轮工作
this.isWork = true;
this.totalSec = this.WORK_TIME;
this.remainSec = this.WORK_TIME;
this.progress = 1.0;
this.statusText = '专注工作';
this.phaseText = 'WORK';
this.buttonText = '开始';
if (this.cycleCount < this.TOTAL_CYCLES) {
this.cycleCount++;
this.cycleText = `第 ${this.cycleCount} / ${this.TOTAL_CYCLES} 轮`;
} else {
// 所有轮次完成
this.cycleCount = 1;
this.cycleText = `第 1 / ${this.TOTAL_CYCLES} 轮`;
this.statusText = '🎉 全部完成!';
}
}
}
模式切换的流程如下:
工作(25min) → 休息(5min) → 工作(25min) → 休息(5min) → ... → 第4轮完成 → 🎉全部完成 → 回到第1轮
关键的边界逻辑:
- 每次切换时,进度条重新置为 100%(
this.progress = 1.0) - 按钮文字恢复为"开始",用户需要手动点击才开始下一阶段
- 4 轮完成后轮次计数器重置,显示"全部完成"
5.5 重置逻辑
重置按钮将应用恢复到初始状态:
resetTimer(): void {
this.stopTimer();
this.isWork = true;
this.cycleCount = 1;
this.totalSec = this.WORK_TIME;
this.remainSec = this.WORK_TIME;
this.progress = 1.0;
this.isRunning = false;
this.buttonText = '开始';
this.statusText = '专注工作';
this.phaseText = 'WORK';
this.cycleText = `第 1 / ${this.TOTAL_CYCLES} 轮`;
}
重置时会先停止计时器(this.stopTimer()),然后重置所有状态变量。这里我踩过一个坑:aboutToAppear 中调用 resetTimer 来初始化,但 aboutToAppear 中不能保证所有成员变量已经完全初始化完毕,所以 resetTimer 中的赋值顺序很重要——先 stopTimer 再设值。
5.6 生命周期管理
在 Ability 的生命周期回调中处理计时器:
aboutToAppear(): void {
this.resetTimer();
}
aboutToDisappear(): void {
this.stopTimer();
}
aboutToAppear 在页面即将显示时调用,这里初始化计时状态;aboutToDisappear 在页面销毁时调用,清理计时器资源防止内存泄漏。这是 ArkUI 组件生命周期管理的基本实践。
六、关键问题与解决方案
6.1 setInterval 精度问题
setInterval 的精度受 JS 引擎事件循环的影响,理论最小精度为 4ms。但对于番茄钟应用来说,秒级精度(1000ms)完全足够,即使有几十毫秒的偏差也无伤大雅。如果需要更高精度的计时(如体育计时),可以使用 @ohos.timer 模块中的定时器 API。
6.2 @State 的响应式边界
ArkTS 的 @State 装饰器只能监听"赋值操作"(=),不能监听对象的属性修改。在我的代码中,所有状态变更都通过赋值完成:
// ✅ 正确:赋值触发重新渲染
this.remainSec--;
this.isRunning = true;
// ❌ 如果 remainSec 是对象:this.remainSec.value = 5 不会触发渲染
这一点在开发过程中要注意,避免使用对象属性修改的方式更新状态。
6.3 Progress 的环形样式配置
在 ArkUI 中,ProgressType.Ring 环形进度条通过 .style({ strokeWidth: 12 }) 配置环的线宽,而不是像普通组件那样通过 .strokeWidth() 属性设置。这个 API 差异最初让我踩了坑——构建时报错 Property 'strokeWidth' does not exist on type 'ProgressAttribute'。查阅文档后发现环形 Progress 的 strokeWidth 确实是 style 对象的一部分。
6.4 文本长度变化导致的布局抖动
当按钮文字从"开始"变为"暂停"时,文字宽度变化可能导致按钮整体宽度变化,进而影响 Row 布局。解决方案:固定按钮宽度(120),确保无论文字如何变化,按钮尺寸不变。
6.5 背景渐变覆盖问题
使用 Stack 层叠时,背景 Column 会作为第一个子组件被后面的内容覆盖。但如果背景在内容后定义,它会盖住内容。因此必须遵循"先背景后内容"的顺序:
Stack() {
Column() { /* 背景渐变 */ } // 先背景
Column() { /* 所有UI内容 */ } // 后内容,叠加在背景之上
}
七、API 24 的新特性利用
7.1 为何选择 API 24
API 24 (HarmonyOS 7.0) 引入了多项对番茄钟应用有益的特性:
- Progress 组件增强:Ring 类型在 API 24 中更加稳定,支持更丰富的样式配置
- 动画性能提升:ArkCompiler 的优化使得 @State 变更驱动的动画更加流畅
- 内存管理优化:更高效的垃圾回收机制,减少长按计时场景中的卡顿
7.2 API 兼容性考虑
在 build-profile.json5 中设置:
"targetSdkVersion": "7.0.0(24)",
"compatibleSdkVersion": "7.0.0(24)"
targetSdkVersion:应用开发和测试的目标 API 版本compatibleSdkVersion:应用兼容的最低 API 版本
两者均为 24 意味着应用只运行在 HarmonyOS 7.0 及以上版本。对于番茄钟这样功能相对独立的应用来说,不需要过度追求向下兼容——API 24 已经覆盖了绝大多数 HarmonyOS Next 设备。
7.3 性能优化建议
在 API 24 环境中,可以利用以下特性优化番茄钟应用的性能:
- 使用
aboutToAppear替代onPageShow:前者在组件创建时执行,后者在每次页面显示时执行。对于计时器初始化,前者更合适。 - 避免在 build 中执行耗时计算:
formatTime这样的纯函数在 build 中调用没问题,但不建议在这里进行复杂运算或 I/O 操作。 - 合理使用
@State:只标记需要触发 UI 重绘的变量,内部计算逻辑使用普通成员变量(如private timerId),避免不必要的渲染开销。
八、测试与验证
8.1 功能测试用例
| 测试用例 | 预期结果 | 状态 |
|---|---|---|
| 点击"开始" | 计时器启动,显示倒计时递减,按钮变为"暂停" | ✅ |
| 点击"暂停" | 计时器暂停,按钮变为"继续" | ✅ |
| 点击"继续" | 计时器从暂停处继续 | ✅ |
| 工作计时归零 | 自动切换到休息模式,进度条重置 | ✅ |
| 休息计时归零 | 自动切换到工作模式,轮次+1 | ✅ |
| 点击"重置" | 回到工作模式初始状态 | ✅ |
| 点击"跳过"(工作时) | 按钮不可点击 | ✅ |
| 点击"跳过"(休息时) | 立即切换到工作模式 | ✅ |
| 完成4轮 | 显示"全部完成",轮次重置 | ✅ |
| 进度条更新 | 随剩余时间线性减少 | ✅ |
8.2 边界条件测试
- 快速点击:快速连续点击"开始/暂停"按钮,计时器状态正确切换
- 多轮循环:连续运行 4 轮以上,确认轮次计数器正确重置
- 页面切换:切换到其他页面再返回,计时器状态保持(或按设计重置)
- 后台运行:鸿蒙 FA 模型下,页面不可见时计时器是否继续运行
九、总结与展望
9.1 开发收获
通过这次番茄钟应用的开发,我深入实践了以下 HarmonyOS Next 开发技术:
- ArkTS 声明式编程:从 TypeScript 语法入门,理解了
@State响应式编程的核心思想 - ArkUI 组件体系:熟练使用了
Stack、Column、Row、Text、Button、Progress等核心组件 - Stage 模型开发:掌握了 Stage 模型的应用架构和生命周期管理
- Hvigor 构建配置:理解了 build-profile 的配置体系和 API 版本管理
9.2 可扩展方向
目前的番茄钟应用只是一个 MVP 版本,还有很多可以扩展的方向:
- 通知提醒:结合
@ohos.notification实现计时结束的系统通知 - 震动反馈:使用
@ohos.vibrator在计时结束时提供触觉反馈 - 统计数据:记录每天/每周的番茄钟完成数量,用图表展示
- 自定义配置:允许用户自定义工作/休息时长和循环次数
- 主题切换:支持多种配色方案
- 分布式协同:利用鸿蒙的分布式能力,在手机和平板间同步状态
- 小组件:开发 HarmonyOS Widget,在桌面上直接显示计时状态
9.3 给其他开发者的建议
如果你也想在 HarmonyOS Next 上开发应用,这里有一些实用建议:
- 先从 ArkTS 开始:如果你有 TypeScript 或 Kotlin 经验,ArkTS 的学习曲线非常平缓
- 善用 DevEco Studio:预览功能可以实时查看 UI 效果,无需每次都真机运行
- 多看官方示例:华为提供了完整的 Sample Code 仓库,包含各种常见场景的最佳实践
- 注意 API 差异:不同 API 版本的组件属性可能有差异,开发时以目标 API 文档为准
- 测试覆盖多设备:鸿蒙生态涵盖手机、平板、车机等多设备形态,提前考虑适配
十、附录:完整源码
10.1 主页面完整代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct Index {
private readonly WORK_TIME: number = 25 * 60;
private readonly BREAK_TIME: number = 5 * 60;
private readonly TOTAL_CYCLES: number = 4;
@State remainSec: number = 25 * 60;
@State isWork: boolean = true;
@State isRunning: boolean = false;
@State cycleCount: number = 1;
@State progress: number = 1.0;
private timerId: number = -1;
private totalSec: number = 25 * 60;
@State statusText: string = '专注工作';
@State buttonText: string = '开始';
@State cycleText: string = '第 1 / 4 轮';
@State phaseText: string = 'WORK';
aboutToAppear(): void { this.resetTimer(); }
aboutToDisappear(): void { this.stopTimer(); }
resetTimer(): void {
this.stopTimer();
this.isWork = true;
this.cycleCount = 1;
this.totalSec = this.WORK_TIME;
this.remainSec = this.WORK_TIME;
this.progress = 1.0;
this.isRunning = false;
this.buttonText = '开始';
this.statusText = '专注工作';
this.phaseText = 'WORK';
this.cycleText = `第 1 / ${this.TOTAL_CYCLES} 轮`;
}
startTimer(): void {
if (this.isRunning) {
this.stopTimer();
this.buttonText = '继续';
this.isRunning = false;
} else {
this.isRunning = true;
this.buttonText = '暂停';
this.timerId = setInterval(() => { this.onTick(); }, 1000);
}
}
stopTimer(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.isRunning = false;
}
onTick(): void {
this.remainSec--;
if (this.remainSec < 0) this.remainSec = 0;
this.progress = this.remainSec / this.totalSec;
if (this.remainSec <= 0) {
this.stopTimer();
this.switchMode();
}
}
switchMode(): void {
if (this.isWork) {
this.isWork = false;
this.totalSec = this.BREAK_TIME;
this.remainSec = this.BREAK_TIME;
this.progress = 1.0;
this.statusText = '休息一下 ☕';
this.phaseText = 'BREAK';
this.buttonText = '开始';
} else {
this.isWork = true;
this.totalSec = this.WORK_TIME;
this.remainSec = this.WORK_TIME;
this.progress = 1.0;
this.statusText = '专注工作';
this.phaseText = 'WORK';
this.buttonText = '开始';
if (this.cycleCount < this.TOTAL_CYCLES) {
this.cycleCount++;
this.cycleText = `第 ${this.cycleCount} / ${this.TOTAL_CYCLES} 轮`;
} else {
this.cycleCount = 1;
this.cycleText = `第 1 / ${this.TOTAL_CYCLES} 轮`;
this.statusText = '🎉 全部完成!';
}
}
}
formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
build() {
Stack() {
// 背景渐变
Column()
.width('100%').height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#1a1a2e', 0.0], ['#16213e', 0.5], ['#0f3460', 1.0]]
})
Column() {
Text('番茄钟 🍅')
.fontSize(28).fontWeight(FontWeight.Bold).fontColor('#e94560')
.margin({ top: 40, bottom: 8 })
Text(this.cycleText).fontSize(16).fontColor('#8899aa')
Text(this.phaseText)
.fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(this.isWork ? '#e94560' : '#4ecca3')
.border({ width: 1, color: this.isWork ? '#e94560' : '#4ecca3', style: BorderStyle.Solid })
.borderRadius(12).padding({ left: 16, right: 16, top: 4, bottom: 4 })
.margin({ top: 12 })
Text(this.statusText).fontSize(20).fontColor('#ccd6e0').margin({ top: 8 })
Stack() {
Progress({
value: this.progress * 100, total: 100, type: ProgressType.Ring
})
.width(260).height(260)
.style({ strokeWidth: 12 })
.color(this.isWork ? '#e94560' : '#4ecca3')
.backgroundColor('rgba(255,255,255,0.08)')
Column() {
Text(this.formatTime(this.remainSec))
.fontSize(56).fontWeight(FontWeight.Bold).fontColor('#ffffff')
.fontFamily('Courier New')
Text(this.isRunning ? '运行中...' : '已暂停')
.fontSize(14).fontColor('#667788').margin({ top: 4 })
}
}.width(260).height(260).margin({ top: 24 })
Row() {
Button('⏹ 重置').fontSize(16).fontColor('#ffffff')
.width(100).height(44)
.backgroundColor('rgba(255,255,255,0.1)').borderRadius(22)
.onClick(() => { this.resetTimer(); })
Blank().width(24)
Button(this.buttonText).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#ffffff')
.width(120).height(52)
.backgroundColor(this.isRunning ? '#e94560' : '#4ecca3').borderRadius(26)
.shadow({
radius: 12,
color: this.isRunning ? 'rgba(233,69,96,0.4)' : 'rgba(78,204,163,0.4)'
})
.onClick(() => { this.startTimer(); })
Blank().width(24)
Button('⏭ 跳过').fontSize(16).fontColor('#ffffff')
.width(100).height(44)
.backgroundColor('rgba(255,255,255,0.1)').borderRadius(22)
.enabled(!this.isWork)
.onClick(() => {
if (!this.isWork) { this.stopTimer(); this.switchMode(); }
})
}.margin({ top: 36 })
Text(this.isWork ? '25分钟专注工作' : '5分钟放松休息')
.fontSize(14).fontColor('#556677').margin({ top: 40 })
}.width('100%').height('100%')
}.width('100%').height('100%')
}
}
10.2 构建配置文件
// build-profile.json5
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "7.0.0(24)",
"compatibleSdkVersion": "7.0.0(24)",
"runtimeOS": "HarmonyOS"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
十一、参考资源



文章字数:约 9,200 字
更多推荐




所有评论(0)