1. 项目缘起与整体设计思路
1.1 为什么要在旧手表上折腾一个番茄钟
手上这块华米Amazfit GTR4已经戴了快两年,屏幕没划痕、电池还能撑一周多,唯一的问题就是——它自带的番茄钟功能太"素"了。系统只给了一个最基础的倒计时,没有任务清单、没有专注统计、没有和待办事项的联动,每次想记录一下今天完成了几个番茄、每个番茄对应什么任务,都得掏出手机手动记,体验割裂得不行。
后来我琢磨了一下,这块表跑的是Zepp OS,官方开放了JS层面的应用开发框架,支持侧载自定义应用。也就是说,我完全可以自己写一个小应用,把番茄钟和待办清单揉在一起,直接跑在手表上。正好那段时间在深度使用AI coding工具辅助开发,就想着拿这个项目练练手——目标很明确:做一个能在GTR4上流畅运行的番茄待办App,功能不用多,但每个功能都得真正用得上。
这个项目适合几类人参考:一是手里有Zepp OS设备、想自己折腾点实用小工具的开发者;二是对AI辅助编程感兴趣、想看看实际落地效果的人;三是单纯想了解智能手表轻量应用开发流程的技术爱好者。不需要你有多深的嵌入式经验,只要会基本的JavaScript,剩下的边做边学完全来得及。
1.2 功能边界的取舍逻辑
做任何小工具,第一件事不是想"能加什么",而是想"必须砍什么"。手表的屏幕就那么大,交互方式只有触摸和侧键,处理器性能和内存也远不如手机。我一开始列了一堆想法:任务分类、优先级、标签、云同步、数据导出……后来全砍了,只保留三个核心模块:
- 番茄计时器:25分钟专注+5分钟休息的经典循环,支持暂停、跳过、重置
- 待办清单:每个番茄可以绑定一个任务名,完成后自动记录
- 专注统计:当天完成的番茄数量、累计专注时长,一屏展示
砍掉的那些功能不是不好,而是放在手表这个场景下性价比太低。比如云同步,手表端做网络请求本身就麻烦,而且我每天看统计也就是扫一眼,根本不需要跨设备同步。再比如任务分类,手机上做分类都嫌烦,手表上更不可能去维护。
这里有个经验:手表应用的黄金法则是"三秒原则"——用户抬腕到完成操作,不应该超过三秒。任何需要超过三次点击才能完成的功能,都值得重新考虑是否要做。
1.3 技术选型与AI coding的介入方式
Zepp OS的应用开发用的是JavaScript,配合官方的Zepp OS SDK,界面用类似JSX的语法描述,整体上手门槛不高。开发工具链方面,官方提供了模拟器和真机调试通道,可以在电脑上先跑通逻辑,再推到手表上验证。
AI coding在这个项目里的角色,不是"帮我写全部代码",而是"帮我快速跨过不熟悉的领域"。我对JS本身很熟,但Zepp OS的API文档结构、页面生命周期、传感器调用方式这些是全新的,AI在这部分帮我省了大量翻文档的时间。具体来说,我的工作流是这样的:
- 先自己理清功能逻辑和页面结构,画出简单的状态流转图
- 把每个页面的需求用自然语言描述给AI,让它生成初版代码骨架
- 自己审查代码,重点看API调用是否符合Zepp OS规范
- 在模拟器上跑,遇到报错把错误信息贴给AI,让它给排查方向
- 真机测试,根据实际表现调整UI尺寸和交互细节
这个流程里,AI承担的是"快速产出可运行草稿"的角色,而判断、调试、优化这些核心环节还是得自己来。下面我把整个开发过程拆开,把每个环节的关键细节都讲清楚。
2. 开发环境搭建与核心细节解析
2.1 Zepp OS开发环境的一次性配置
Zepp OS的开发环境搭建不算复杂,但有几个坑我踩过,提前说清楚能省不少时间。首先你需要安装Node.js,版本建议在16以上,然后通过npm安装官方的CLI工具。安装命令大概是这样:
npm install -g @zeppos/zeus-cli装完之后用zeus create命令创建一个新项目,CLI会引导你选择项目模板。这里建议选"Empty App"模板,不要选带示例代码的模板,因为示例代码里有很多你用不上的东西,反而干扰理解。
创建完项目后,目录结构大致是这样的:app.json是全局配置,app.js是应用入口,page目录下放各个页面。每个页面由三个文件组成:.js负责逻辑,.ux负责界面描述,.json负责页面配置。这个结构和很多前端框架类似,有前端经验的人看起来会很亲切。
注意:CLI工具在Windows上的路径处理偶尔会有问题,如果你在Windows上遇到命令找不到的情况,试试用管理员权限重新打开终端,或者检查一下npm的全局路径是否加到了系统环境变量里。
2.2 页面结构设计与状态管理
这个App我一共设计了三个页面:主页(番茄计时器)、任务页(待办清单)、统计页(专注数据)。三个页面之间用官方的路由API跳转,状态数据统一放在一个全局的store对象里管理。
为什么不用每个页面各自管理状态?因为番茄计时器的状态需要在页面切换时保持——比如你正在专注,切到任务页看一下待办,再切回来,计时不能断。如果状态放在页面内部,页面销毁时数据就丢了。用一个全局store,所有页面读写同一份数据,逻辑清晰也不容易出bug。
store的结构大概是这样:
const store = { timer: { status: 'idle', // idle / running / paused / break remaining: 1500, // 剩余秒数 currentTask: '', // 当前绑定的任务 completedCount: 0 // 今日完成数 }, tasks: [], // 待办列表 stats: { date: '', totalFocus: 0, sessions: [] } }这个结构看着简单,但每个字段的存在都有理由。status用字符串而不是数字,是为了代码可读性,调试的时候一眼就能看出当前是什么状态。remaining用秒而不是毫秒,是因为手表端做毫秒级计时没有意义,反而增加计算负担。
2.3 计时器精度的处理方案
计时器是这个App的核心,精度问题必须处理好。最朴素的做法是用setInterval每秒减一,但这种方式在手表上有两个问题:一是setInterval在后台可能被系统挂起,导致计时不准;二是长时间运行后累积误差会越来越大。
我的方案是记录一个"开始时间戳",每次刷新时用当前时间减去开始时间,算出实际经过的秒数,再更新剩余时间。这样即使定时器被挂起几次,恢复后也能立刻校正到正确的时间。具体逻辑:
// 开始计时时记录 store.timer.startTimestamp = Date.now() store.timer.startRemaining = store.timer.remaining // 每次刷新时计算 const elapsed = Math.floor((Date.now() - store.timer.startTimestamp) / 1000) store.timer.remaining = store.timer.startRemaining - elapsed这个方案的好处是,计时精度只取决于系统时间,不依赖定时器的准确性。实测下来,连续跑25分钟,误差在1秒以内,完全够用。
实操心得:Zepp OS的定时器最小间隔建议不要低于500毫秒,太频繁的刷新会明显增加耗电。我用的是1秒刷新一次,配合时间戳校正,既省电又准确。
3. 核心功能实现与实操过程
3.1 番茄计时器的完整实现
计时器的界面很简单:中间一个大号的时间数字,下面两个按钮(开始/暂停、重置),顶部显示当前绑定的任务名。但简单界面背后的逻辑分支不少,我把状态机画清楚之后才动手写代码。
状态流转是这样的:idle状态下点击开始,进入running;running状态下点击暂停,进入paused;paused状态下点击继续,回到running;计时归零后,如果是专注阶段就进入break,如果是休息阶段就回到idle并完成计数。
界面刷新用Zepp OS提供的setInterval配合页面生命周期。这里有个细节:页面onShow的时候启动定时器,onHide的时候要清掉,否则页面切走后定时器还在跑,白白耗电。但清掉定时器不代表计时停止,因为我们的计时是基于时间戳的,重新onShow时再启动定时器,时间会自动校正。
Page({ onShow() { this.timerId = setInterval(() => { this.updateTimer() }, 1000) }, onHide() { clearInterval(this.timerId) }, updateTimer() { if (store.timer.status !== 'running') return const elapsed = Math.floor((Date.now() - store.timer.startTimestamp) / 1000) const remaining = store.timer.startRemaining - elapsed if (remaining <= 0) { this.handleComplete() } else { store.timer.remaining = remaining this.refreshUI() } } })handleComplete里处理计时结束的逻辑:播放震动提醒、更新完成计数、切换阶段。震动用@zeppos/sensor里的振动接口,给一个短促的震动就行,不用太强。
3.2 待办清单的数据结构与交互
待办清单我设计得很轻量:一个数组,每个元素是一个对象,包含任务名、创建时间、是否完成、绑定的番茄数。界面上就是一个可滚动的列表,点击某一项就把它设为"当前任务",计时器会自动绑定这个任务。
数据结构:
{ id: 'task_001', name: '写项目文档', created: 1699000000000, done: false, pomodoros: 0 }添加任务的交互我纠结了很久。手表上打字太痛苦了,所以最终方案是:手表端只支持从预设的常用任务里选,或者快速添加"任务1、任务2"这种占位名,真正的任务编辑放在手机端的配套设置里。这样虽然牺牲了一点灵活性,但换来了手表端操作的流畅性。
踩过的坑:一开始我想在手表上做全键盘输入,结果发现屏幕太小,按键根本点不准,而且输入法切换很麻烦。后来改成预设列表+语音输入(调用系统语音转文字),体验好了很多。如果你的设备支持语音输入,强烈建议用这个方案。
列表的滚动用官方的scroll组件,注意设置好scroll-top和scroll-height,否则在真机上可能出现滚动不流畅或者内容被截断的问题。每一项的高度我设的是80像素,这个尺寸在GTR4的1.43英寸屏幕上刚好,手指点起来不费劲。
3.3 专注统计的数据记录与展示
统计页展示三个数据:今日完成番茄数、今日累计专注时长、最近七天的完成趋势。前两个数据实时从store里读,第三个需要持久化存储。
Zepp OS提供了本地存储API,可以存一些简单的键值对。我每天结束时把当天的统计数据存进去,键名用日期,这样查历史数据的时候直接按日期读就行。
// 保存当日统计 const today = formatDate(new Date()) localStorage.setItem(`stats_${today}`, JSON.stringify(store.stats)) // 读取最近七天 const recent = [] for (let i = 0; i < 7; i++) { const d = formatDate(new Date(Date.now() - i * 86400000)) const data = localStorage.getItem(`stats_${d}`) recent.push(data ? JSON.parse(data) : { totalFocus: 0, sessions: [] }) }趋势展示我用的是简单的柱状图,用矩形组件堆出来的,没有引入图表库——手表端引入第三方库的代价太大,而且这么简单的图表自己画反而更可控。每根柱子的高度按当天专注时长占七天最大值的比例来算,最低给一个10像素的保底高度,避免时长为零时柱子完全看不见。
3.4 真机调试与性能优化实录
模拟器上跑通不代表真机没问题。我第一次把应用推到手表上,发现了三个问题:一是启动速度慢,从点击图标到界面出现大概要两秒多;二是计时页面滑动切换时偶尔卡顿;三是连续运行半小时后,手表明显发热。
针对启动慢,我做了代码分割,把统计页的代码改成按需加载,不在一开始就全部初始化。这个改动让启动时间缩短到了1秒以内。针对卡顿,我把页面切换动画从默认的滑动改成了淡入淡出,减少了渲染压力。针对发热,我检查了定时器的刷新频率,把UI刷新从每秒一次改成了每两秒一次(时间戳校正保证了计时精度不受影响),同时把不必要的传感器监听全部关掉。
优化前后的对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 启动时间 | 2.3秒 | 0.8秒 |
| 页面切换帧率 | 约24fps | 约45fps |
| 半小时耗电 | 约8% | 约3% |
经验之谈:手表端开发,性能优化的优先级是"减少渲染 > 减少计算 > 减少IO"。任何能合并的UI刷新都合并,任何能缓存的中间结果都缓存,本地存储的读写频率要严格控制。
4. 常见问题排查与避坑指南
4.1 开发过程中遇到的典型报错
问题一:页面白屏,控制台无报错。这种情况大概率是.ux文件里的组件引用路径写错了。Zepp OS对路径大小写敏感,./components/Timer和./components/timer会被当成两个不同的路径。排查方法是逐个检查import语句,确保路径和实际文件名完全一致。
问题二:定时器在后台不执行。这是预期行为,Zepp OS为了省电,应用切到后台后定时器会被挂起。解决办法就是前面说的,用时间戳校正,不要依赖定时器的连续性。如果你的功能确实需要后台运行,可以研究一下官方的后台任务API,但要注意后台任务的执行频率有限制。
问题三:本地存储写入失败。检查一下存储的键名是否包含特殊字符,以及存储的值是否超过了大小限制。Zepp OS的本地存储单个键值对建议不要超过1KB,总容量也有限。如果数据量大,考虑分片存储或者只存必要的摘要数据。
问题四:真机上字体显示不全。手表屏幕的像素密度和模拟器不一样,模拟器上看着刚好的字号,真机上可能被裁切。解决办法是字号用相对单位而不是绝对像素,或者针对不同设备做适配。GTR4的分辨率是466x466,我最终用的字号是32px(大数字)和20px(正文),实测显示效果最好。
4.2 AI coding辅助开发的正确姿势
用AI辅助写代码,最大的陷阱是"盲目信任"。AI生成的代码在语法上通常没问题,但在API的具体用法上经常出错——它可能会用一些看起来合理但实际不存在的API,或者参数顺序搞反。我的做法是:AI生成的每一段涉及平台API的代码,都要对照官方文档验证一遍。
另一个心得是,给AI的描述越具体,产出质量越高。比如不要说"帮我写一个计时器页面",而要说"帮我写一个Zepp OS的计时器页面,使用setInterval每秒刷新,页面onShow时启动定时器,onHide时清除,计时基于时间戳校正"。后者生成的代码基本可以直接用,前者生成的代码需要大量修改。
还有一个技巧:把报错信息完整地贴给AI,包括堆栈信息。AI对错误信息的理解能力很强,往往能直接定位到问题所在。但要注意,贴报错信息之前先把里面可能包含的敏感路径或者个人信息删掉。
4.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 应用启动崩溃 | 入口文件语法错误 | 检查app.js和app.json | 用模拟器逐步调试,定位到具体行 |
| 界面元素错位 | 布局用了绝对定位 | 检查.ux文件的样式 | 改用flex布局,用百分比而非固定像素 |
| 计时不准 | 依赖setInterval累加 | 检查计时逻辑 | 改用时间戳校正方案 |
| 数据丢失 | 未做持久化 | 检查存储读写时机 | 在onHide和计时完成时写入存储 |
| 耗电过快 | 定时器频率过高 | 检查setInterval间隔 | 降低刷新频率,关闭无用传感器 |
| 真机与模拟器表现不一致 | 设备差异 | 对比分辨率和性能 | 真机实测,针对性适配 |
4.4 几个让我少走弯路的小技巧
第一个技巧:开发阶段把日志打全一点。Zepp OS的日志可以在CLI终端里看到,用console.log输出关键状态,排查问题时能省很多时间。但记得发布前把日志清理掉,过多的日志输出会影响性能。
第二个技巧:善用模拟器的"慢速模式"。模拟器可以模拟低性能设备,在这个模式下跑通了,真机上基本不会卡。我习惯在慢速模式下做功能测试,在正常模式下做体验测试。
第三个技巧:版本管理要趁早。哪怕是一个人开发的小项目,也建议用Git管理起来。每次改完一个功能就提交一次,出问题的时候可以快速回滚。AI生成的代码尤其要这样,因为有时候AI的修改会引入一些隐蔽的bug,有版本记录才能快速定位。
第四个技巧:真机测试要覆盖完整的使用场景。不要只测"打开应用-开始计时-结束计时"这个主流程,还要测"计时中途切到其他应用再切回来"、"计时中途息屏再亮屏"、"连续使用一小时后"这些边界情况。很多bug只有在这些场景下才会暴露。
5. 项目复盘与后续扩展思路
5.1 这个项目教会我的几件事
做完这个项目,我最大的感受是:AI coding确实能大幅提升开发效率,但它改变的是"写代码"这个环节的速度,而不是"想清楚要做什么"和"验证做得对不对"这两个环节。整个项目里,我花在理清需求和调试上的时间,其实比写代码的时间还多。AI帮我省下的是查文档、写样板代码的时间,这部分大概占了总工作量的四成左右。
另一个感受是,手表端开发和手机端、网页端开发的思维差异很大。手机端可以容忍一定的性能浪费,手表端不行;手机端可以堆功能,手表端必须做减法。这种约束反而让设计变得更清晰——当你只有三个功能可以做的时候,你会认真思考哪三个才是真正重要的。
5.2 后续可以怎么扩展
这个App目前只实现了最核心的功能,后续有几个方向可以继续做。一是增加"白噪音"功能,在专注时播放一些环境音,帮助进入状态,这个需要用到音频播放API。二是增加"任务模板",把常用的任务组合保存下来,一键开始一整组番茄。三是做手机端的配套应用,把手表上的数据同步到手机,在手机上做更详细的分析和回顾。
不过这些扩展都要遵循一个原则:不能影响核心体验。手表应用最怕的就是功能越加越多,最后变得又慢又难用。每加一个功能,都要问自己:这个功能值得用户多花三秒钟吗?如果答案是否定的,那就不加。
5.3 给想尝试类似项目的人的建议
如果你也想给自己的手表写一个应用,我的建议是从最小的功能开始。不要一上来就想做一个"全能助手",先做一个"能用的计时器",跑通了再慢慢加东西。开发过程中遇到问题,先查官方文档,文档解决不了的再问AI,AI给的方案一定要自己验证。
还有一点很重要:不要追求完美。我的这个App界面很朴素,没有动画、没有花哨的配色,但它稳定、省电、该有的功能都有。对于手表应用来说,稳定和省电比好看重要得多。先把核心功能做扎实,美化的事情可以后面再说。
最后,享受这个过程。给自己的设备写一个真正用得上的工具,那种"这东西是我做的"的满足感,是直接用现成应用体会不到的。而且做完之后你会发现,你对这块手表的理解,比之前深了不止一个层次。