起因:我想知道我的精力都花哪儿了
去年有段时间状态特别差,每天下班回家觉得被掏空了,但又说不清到底是什么在消耗我。
试过几个情绪记录类的 App,打开就是让你选「开心/难过/焦虑」,说实话我大部分时间也没什么明确情绪,就是「累」。这些工具对我来说太重了,也不对路。
后来我换了个思路:不记情绪,记「电量」。
就像手机电池一样——早上满电出门,开了个会掉 20%,午休充回来 15%,下午写代码又掉 30%。这个模型特别直觉,不需要你分析自己到底是焦虑还是烦躁,就一个问题:这件事让你掉电了还是充电了?掉了多少?
这就是「元気手账」的核心想法。iOS 版做完之后,鸿蒙版也上架了。今天主要聊聊鸿蒙这边的实现。
数据模型:从 SwiftData 到 relationalStore 的迁移
iOS 端我用的 SwiftData,模型很简单,核心就三个实体:EnergyRecord(精力记录)、Project(方舟项目)、QuickAction(快捷动作)。
一条精力记录长这样:标题 + delta 值(正数是充电,负数是耗电)+ 时间戳 + 可选关联的项目。每天还有一个 DailyBaseline,记录当天的初始电量(默认 50),所有 delta 累加上去就是实时电量。
迁移到鸿蒙端的时候,我用了 @ohos.data.relationalStore 来做持久化。这个过程踩了两个具体的坑:
第一个坑是表结构升级的版本管理。 SwiftData 的自动迁移太惯坏人了,到了鸿蒙你得自己管 storeConfig 里的版本号。我在 1.1.0 给 Project 表加了 totalSeconds 字段,但忘了在 onUpgrade 回调里写 ALTER TABLE 语句,结果老用户升级直接查询报错,字段不存在。后来老老实实写了版本判断逻辑:
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'BatteryLog.db',
securityLevel: relationalStore.SecurityLevel.S1
};
// onUpgrade 里按版本号逐级迁移
function onUpgrade(store: relationalStore.RdbStore, oldVersion: number, newVersion: number) {
if (oldVersion < 2) {
store.executeSql('ALTER TABLE project ADD COLUMN total_seconds INTEGER DEFAULT 0');
}
if (oldVersion < 3) {
store.executeSql('ALTER TABLE quick_action ADD COLUMN is_pinned INTEGER DEFAULT 0');
}
}
```
**第二个坑是外键关联。** SwiftData 里 `EnergyRecord` 和 `Project` 的关系用 `@Relationship` 一行搞定,反向关系也自动建好了。鸿蒙端得自己在 `energy_record` 表里加 `project_id` 字段,查询时手动 JOIN,删除项目时还得自己处理级联置空。说白了就是回到了手写 SQL 的时代——可控是可控,但工作量多了不少。
## 时间轴和电量曲线:ArkUI Canvas 绘制
App 里最核心的视图是时间轴——一条竖线,从早到晚,每个节点是一次精力变化,红色掉电绿色充电,右边显示累计电量。
iOS 端用 SwiftUI 的 `Canvas` 画的,鸿蒙端我用了 ArkUI 的 `Canvas` 组件,通过 `onReady` 回调拿到 `CanvasRenderingContext2D` 来绘制电量趋势曲线。
要实现每段线条根据 delta 正负显示不同颜色,每一段得单独 `beginPath` + `stroke`,不能一条路径画到底再统一描边(那样只会用最后设置的颜色)。核心逻辑大概是这样:
```arkts
Canvas(this.bindctx)
.width('100%')
.height(200)
.onReady(() => {
let ctx = this.ctx
let baseline = 50
let prevX = 0, prevY = this.energyToY(baseline)
for (let i = 0; i < this.records.length; i++) {
baseline += this.records[i].delta
let x = this.timeToX(this.records[i].timestamp)
let y = this.energyToY(baseline)
ctx.beginPath()
ctx.moveTo(prevX, prevY)
ctx.lineTo(x, y)
ctx.strokeStyle = this.records[i].delta > 0 ? '#34C759' : '#FF3B30'
ctx.stroke()
prevX = x; prevY = y
}
})
```
这里 `energyToY` 把电量值映射到画布纵坐标,`timeToX` 把时间戳映射到横坐标。每段独立绘制,充电是绿色,掉电是红色。
有个坑得提一下:`Canvas` 放在 `Scroll` 容器里的时候,滑动事件会冲突,手指在曲线区域滑动时 Scroll 也会响应。我最后用 `.priorityGesture()` 给 Canvas 区域加了手势优先级才解决,花了差不多一个下午调这个。
## 快捷动作条:降低记录门槛是生死线
精力记录这种东西,如果每次都要打开 App → 点新建 → 输标题 → 选数值 → 保存,三天就没人用了。
所以我做了一个「快捷动作条」,常用的记录模板(比如「开会 -15」「午睡 +20」「跑步 +25」)横向排列在首页顶部,点一下就完成记录。
鸿蒙端这个动作条用了 `List` 的横向模式配合 `ListItem`,每个动作绑定了 `onClick` 直接写库。交互层面尽量少跳页面,记录完一个轻振动反馈就行。
这个动作条上线之后,我自己的使用频率明显提高了。之前懒得打开 App 的场景,现在两秒就记完了。
## 方舟项目:精力管理和目标管理打通
「方舟项目」是我觉得跟其他同类工具差异最大的点。
普通的精力/情绪记录 App,记完就记完了,你知道今天累但不知道累在哪。方舟项目让你把每次精力消耗关联到一个具体的项目上——工作、写作、健身、带娃,随便你怎么分。
我自己记了三周,大概 85 条记录。看项目维度的统计时发现:「开会」关联的记录有 28 条,吃掉了将近 35% 的精力总消耗,但翻了一下这 28 条里有 15 条标注的是旁听类会议(周会、同步会),基本没有我要输出的环节。这个数据让我开始有意识地拒绝一些不必要的会议邀请,把周三和周五下午的两个例行同步会退掉了。
还有一个发现是「写代码」其实没有我以为的那么耗电,真正耗电的是「在不确定的事情之间来回切换」。
项目还有经验值和等级系统(1-10 级),投入越多等级越高,满级变成「传奇」。这个设计说白了就是个小游戏化的激励,但确实管用——我的「健身」项目已经 5 级了,要是哪天不记录总觉得亏了。
## 纯本地数据,没有服务端
这个得单独说一下。元気手账所有数据都存在本地,没有云同步,没有账号体系。
不是我不想做,是我觉得精力数据太私密了。你的低谷时段、每天的能量走势、哪些事最消耗你——这些东西比步数和心率更能暴露一个人的真实状态。我不想替用户承担这个数据泄露的风险。
鸿蒙端用 `@ohos.data.relationalStore` 存储,数据就在设备沙箱目录里,`SecurityLevel` 设的 `S1`。
## 当前状态
元気手账 1.2.0 已经在鸿蒙应用市场上架了。
说实话数据很惨淡,目前下载量还很少。独立开发者没有推广预算,鸿蒙市场的自然流量也还在成长期,这些我心里有数。但我自己每天在用,这个工具确实帮我发现了一些之前注意不到的模式——比如每周三下午精力固定低谷(后来想明白了是周三上午连着两个会的后遗症),发现之后我把周三下午改成只做机械性工作,状态好了不少。
## 一个问题想请教
做鸿蒙适配过程中感触最深的就是数据层。ArkUI/ArkTS 的 UI 开发体验在快速进步,但数据库这块跟 iOS 的 SwiftData 比还是原始了不少,特别是表结构迁移,基本全靠手动 SQL。
想问问同样在做 HarmonyOS 开发的各位:**你们处理 relationalStore 的版本升级和数据迁移是怎么做的?有没有比逐版本 ALTER TABLE 更好的方案?**
---
鸿蒙应用市场搜「元気手账」可以体验,如果你也在做鸿蒙开发,欢迎交流。
1万+



被折叠的 条评论
为什么被折叠?



