归档、恢复还是删除?HarmonyOS 习惯数据生命周期设计
为什么习惯不能只有一个删除按钮
用户不再继续“每天阅读”时,可能有两种完全不同的意图:
暂时不做了,但想保留历史
适合归档。
这个习惯建错了,希望连历史一起删除
适合删除。
如果产品只有删除,用户为了整理今日列表就必须牺牲历史;如果所谓归档只是隐藏 UI,但统计仍持续把未来日期算进分母,数据又会越来越失真。
一、Habit 模型需要表达生命周期
export interface Habit {
id: string;
name: string;
icon: string;
color: string;
isActive: boolean;
createdAtMillis: number;
updatedAtMillis: number;
archivedAtMillis: number;
}
其中:
isActive:当前是否显示在今日打卡;createdAtMillis:从哪天开始参与统计;archivedAtMillis:从哪天停止参与;updatedAtMillis:列表刷新和审计使用。
二、今日页只关心 activeHabits
private activeHabits(): Habit[] {
return this.habits.filter(
(habit: Habit) => habit.isActive
);
}
归档列表则相反:
private archivedHabits(): Habit[] {
return this.habits.filter(
(habit: Habit) => !habit.isActive
);
}
归档改变的是“当前是否继续参与”,不是删除实体。
三、归档时要记录发生日期
private setHabitActive(
habitId: string,
active: boolean
): void {
const now: number = Date.now();
this.habits = this.habits.map((habit: Habit) => {
if (habit.id !== habitId) {
return habit;
}
return {
id: habit.id,
name: habit.name,
icon: habit.icon,
color: habit.color,
isActive: active,
createdAtMillis: habit.createdAtMillis,
updatedAtMillis: now,
archivedAtMillis: active ? 0 : now
};
});
this.persist();
this.refreshUi();
}
归档:
isActive = false
archivedAtMillis = now
恢复:
isActive = true
archivedAtMillis = 0
四、历史打卡为什么不能随归档删除
HabitCompletion 独立保存:
export interface HabitCompletion {
id: string;
habitId: string;
dayIdentifier: string;
isCompleted: boolean;
// 时间字段
}
归档时不修改 habitCompletions,因此历史月历和最近记录仍能找到过去的完成情况。
这是归档的核心承诺:
停止未来展示,保留过去事实
五、统计分母只计算有效生命周期
假设一个习惯 8 月 10 日创建,8 月 15 日归档。查看最近 30 天时,不应该把 30 天全部当作可完成天数。
项目逐日构造可参与日期:
if (
dayStart >= createdStart &&
(
archivedStart === 0 ||
dayStart <= archivedStart
)
) {
identifiers.push(dayIdentifier(day));
}
因此分母只包含:
创建日 ≤ 日期 ≤ 归档日
新建习惯不会为创建前的日期得到大量“未完成”,归档后也不会继续拉低完成率。
六、恢复后单个 archivedAtMillis 有什么局限
当前模型恢复时把 archivedAtMillis 清零。
如果一个习惯经历:
创建 → 归档 10 天 → 恢复
恢复后,模型无法再知道中间曾暂停 10 天。统计会把整个创建日至今都视为有效期。
对于第一版轻量习惯模型,这个取舍可以接受;如果产品支持频繁暂停/恢复,就需要更完整的区间模型:
interface HabitActivePeriod {
habitId: string;
startDay: string;
endDay: string;
}
或者保存状态变更事件:
ACTIVATED
ARCHIVED
RESTORED
统计时根据事件重建有效区间。
这说明时间字段设计要与产品承诺匹配。只存最后一次归档时间,不能支持无限复杂的历史解释。
七、删除必须级联清理关联打卡
删除实现:
private deleteHabit(habitId: string): void {
this.habits = this.habits.filter(
(habit: Habit) => habit.id !== habitId
);
this.completions = this.completions.filter(
(completion: HabitCompletion) =>
completion.habitId !== habitId
);
this.showDeleteHabitId = '';
this.persist();
this.refreshUi();
}
如果只删除 Habit,会留下孤儿 Completion:
- 历史日期仍可能出现打卡数量;
- 统计可能计算一个不存在的习惯;
- JSON 导出包含无法解释的 habitId;
- 以后导入数据库时破坏引用完整性。
本地数组模型没有数据库外键,因此级联规则必须由 Repository 或业务层主动保证。
八、删除前为什么要二次确认
归档可恢复,删除不可恢复,交互强度应不同。
删除确认文案需要说明影响范围:
习惯及相关打卡会永久删除;如果以后可能继续,建议选择归档。
它比“确定删除吗?”提供了更有价值的信息:
- 告诉用户会删除关联打卡;
- 提醒存在更温和的归档选项;
- 让不可逆操作不容易误触。
九、删除全部数据与删除单个习惯不同
删除全部数据会清空:
this.moodEntries = [];
this.habits = [];
this.completions = [];
并保持“默认习惯已经初始化过”的标志,避免重启后自动复活。
删除单个习惯只影响该 Habit 和关联 Completion,不应该删除当天的心情日记或其他习惯。
不同删除范围必须在函数和文案中分开,不要复用一个含糊的 clear()。
十、什么时候应该提供撤销
如果用户误删成本高,可以考虑:
软删除
增加 deletedAtMillis,一段时间后物理清理。
操作后撤销
短时间保留被删除对象和关联打卡,Toast 提供“撤销”。
导出后删除
删除全部数据前提示先导出 JSON。
但不要只在 UI 隐藏记录,却在隐私文案中声称已经永久删除。产品语义、磁盘状态和用户承诺必须一致。
十一、生命周期测试清单
归档
- 今日页立即消失;
- 历史打卡仍存在;
- 归档列表出现;
- 统计截止到归档日;
- 杀进程后仍归档。
恢复
- 今日页重新出现;
- 历史打卡仍存在;
- 新打卡可以创建;
- 统计口径符合当前模型定义。
删除
- 二次确认可取消;
- Habit 删除;
- 关联 Completion 全部删除;
- 其他习惯不受影响;
- 历史日期和统计同步刷新;
- 导出 JSON 不再含孤儿引用。
总结
归档、恢复和删除的区别可以归纳为:
| 操作 | 今日展示 | 历史打卡 | 统计未来分母 | 可恢复 |
|---|---|---|---|---|
| 归档 | 移除 | 保留 | 停止计入 | 是 |
| 恢复 | 恢复 | 保留 | 重新计入 | — |
| 删除 | 移除 | 删除 | 不再存在 | 否 |
只有当数据模型、统计口径和交互文案同时遵守这张表,习惯生命周期才真正一致。
本文案例来自“心晴手记(MoodMemoir)”HarmonyOS 版的习惯管理与统计实现。
更多推荐



所有评论(0)