Files
habo/docs/03-SPECIFICATION.md
dazhuang aa69f2a91e feat: initial commit - Habo habit tracking app
- Complete MVP with Repository Pattern, SQLite storage
- Provider + ChangeNotifier state management
- Navigation 2.0 with deep link support
- Habit CRUD with twoDayRule, notifications, categories
- Backup/Restore via JSON
- Statistics with streak tracking
- Material You theme support
- Biometric lock support
- Desktop widget support
- 27 languages i18n structure
- Comprehensive test suite (87/89 passing)
2026-04-13 15:02:30 +00:00

30 KiB
Raw Permalink Blame History

Habo 实现规格文档

基于 REQUIREMENTS.md 和 ARCHITECTURE.md定义每个功能的具体实现逻辑、算法和交互流程


1. 连续天数 (Streak) 算法

1.1 普通模式 (_updateLastStreakNormal)

输入: habitData.events (SplayTreeMap<DateTime, List>, 按日期升序排列)
输出: streak 值, streakVisible (bool), orangeStreak (bool)

算法:
1. 从最后一天开始,向前遍历 events
2. 初始化 inStreak = 0
3. 对每个事件(从后往前):
   a. 如果 DayType == clear → 跳过
   b. 如果日期间隔 > 1 天 → 断开,结束循环
   c. 如果 DayType == check → inStreak++
   d. 如果 DayType == progress (数值型):
      - 如果 progressValue >= targetValue → inStreak++
      - 否则 → 跳过
   e. 如果 DayType == fail 或 skip → 断开,结束循环
4. streak = inStreak
5. streakVisible = (streak >= 2)
6. orangeStreak = false

1.2 两天法则模式 (_updateLastStreakTwoDay)

输入: habitData.events, habitData.twoDayRule == true
输出: streak 值, streakVisible, orangeStreak

算法:
1. 从最后一天开始,向前遍历 events
2. 变量: inStreak = 0, usingTwoDayRule = false
3. 对每个事件(从后往前):
   a. 如果 DayType == clear → 跳过
   b. 如果日期间隔 > 1 天 → 断开,结束循环
   c. 如果 DayType == check → inStreak++, usingTwoDayRule = false
   d. 如果 DayType == progress (数值型) 且 progress >= target → inStreak++, usingTwoDayRule = false
   e. 如果 DayType == fail:
      - 如果 usingTwoDayRule == true → 断开,结束循环(连续两天失败)
      - 如果 usingTwoDayRule == false → usingTwoDayRule = true, 不增加 inStreak
   f. 如果 DayType == skip:
      - 如果 usingTwoDayRule == true → 断开
      - 否则 → 跳过(不影响连续)
4. streak = inStreak
5. streakVisible = (streak >= 2)
6. orangeStreak = usingTwoDayRule  ← 橙色表示"处于危险中"

两天法则图解

情况 1: ✅ ✅ ❌ ✅ ✅ → streak = 5 (✅ 失败一天后立即恢复)
情况 2: ✅ ✅ ❌ ❌ ✅ → streak = 0 (✅ 连续两天失败,归零)
情况 3: ✅ ✅ ❌ ⏭ ✅ → streak = 0 (⏭ 在两天法则期间跳过,归零)
情况 4: ✅ ✅ ⏭ ✅ ✅ → streak = 5 (⏭ 跳过不影响连续)

2. 日历事件交互流程

2.1 日历日期点击

用户点击日历日期
  │
  ├── 检查 oneTapCheck 设置
  │     │
  │     ├── oneTapCheck == true (一键打卡模式):
  │     │     │
  │     │     ├── 布尔型习惯:
  │     │     │     ├── 当前无事件 → 创建 [DayType.check, ""]
  │     │     │     └── 当前有事件 → 删除事件 (设为 clear)
  │     │     │
  │     │     └── 数值型习惯:
  │     │           ├── 当前无事件 → 创建 [DayType.check, "", targetValue, targetValue]
  │     │           └── 当前有事件 → 删除事件
  │     │
  │     └── oneTapCheck == false (菜单模式):
  │           │
  │           └── 弹出选择菜单6 个选项:
  │                 │
  │                 ├── 📅 Date → 修改日期选择器
  │                 │
  │                 ├── ✅ Check → 标记完成
  │                 │     ├── 布尔型: 事件 = [DayType.check, ""]
  │                 │     └── 数值型: 事件 = [DayType.check, "", targetValue, targetValue]
  │                 │     └── 播放 check 音效
  │                 │     └── 如果 showReward → 显示奖励通知
  │                 │
  │                 ├──  Plus/Progress → 数值型专用
  │                 │     └── 弹出 ProgressInputModal
  │                 │           ├── 圆形进度指示器 (120px)
  │                 │           ├── 当前值 / 目标值 显示
  │                 │           ├── 快捷按钮: +partialValue, -partialValue
  │                 │           ├── 直接输入文本框
  │                 │           └── "Complete" 按钮直接设为目标值
  │                 │
  │                 ├── ❌ Fail → 标记失败
  │                 │     └── 事件 = [DayType.fail, ""]
  │                 │     └── 播放 click 音效
  │                 │     └── 如果 showSanction → 显示惩罚通知
  │                 │
  │                 ├── ⏭ Skip → 标记跳过
  │                 │     └── 事件 = [DayType.skip, ""]
  │                 │     └── 播放 click 音效
  │                 │
  │                 └── 💬 Note → 添加备注
  │                       └── 弹出文本输入对话框
  │                       └── 保留原有事件类型,更新 comment
  │
  ▼
HabitsManager.addEvent(habitId, date, eventData)
  │
  ├── 更新内存: habitData.events[date] = eventData
  ├── 写入数据库: EventRepository → SQLite REPLACE
  ├── 更新连续天数: _updateLastStreak()
  ├── 更新桌面小组件: HomeWidgetService.update()
  └── notifyListeners() → UI 重建

2.2 事件数据结构

// 布尔型习惯
[DayType.check, ""]           // 完成
[DayType.fail, ""]            // 失败
[DayType.skip, ""]            // 跳过
[DayType.check, "好的开始"]   // 完成 + 备注

// 数值型习惯
[DayType.check, "", 5.0, 5.0]       // 完成 (5/5 km)
[DayType.progress, "", 3.5, 5.0]    // 部分进度 (3.5/5 km)
[DayType.fail, ""]                   // 失败

// 数组索引:
// [0] = DayType (枚举)
// [1] = comment (String)
// [2] = progressValue (double, 数值型)
// [3] = targetValue (double, 数值型)

3. 统计计算算法

3.1 数据结构

class StatisticsData {
  String title;                          // 习惯标题
  int topStreak = 0;                     // 最高连续天数
  int actualStreak = 0;                  // 当前连续(遍历中)
  int checks = 0;                        // 完成次数
  int fails = 0;                         // 失败次数
  int skips = 0;                         // 跳过次数
  int progress = 0;                      // 进度次数
  SplayTreeMap<int, Map<DayType, List<int>>> monthlyTracking;
  // key = year * 100 + month (如 202604)
  // value = { DayType: [day1, day2, ...] }
}

class OverallStatisticsData {
  int totalChecks;
  int totalFails;
  int totalSkips;
  int totalProgress;
}

3.2 计算流程

Statistics.calculateStatistics(habits):
  │
  ├── 1. 创建 AllStatistics 容器
  │
  ├── 2. 遍历每个 habit:
  │     │
  │     ├── 创建 StatisticsData
  │     │
  │     ├── 3. 遍历 events (按日期升序):
  │     │     │
  │     │     ├── 计算日期间隔:
  │     │     │     └── 如果间隔 > 1 天 → actualStreak 归零
  │     │     │
  │     │     ├── DayType.check:
  │     │     │     ├── checks++
  │     │     │     ├── actualStreak++
  │     │     │     └── if actualStreak > topStreak → topStreak = actualStreak
  │     │     │
  │     │     ├── DayType.progress:
  │     │     │     ├── progress++
  │     │     │     └── if 数值型 && progressValue >= targetValue:
  │     │     │           ├── actualStreak++
  │     │     │           └── update topStreak
  │     │     │
  │     │     ├── DayType.fail:
  │     │     │     ├── fails++
  │     │     │     └── if twoDayRule:
  │     │     │           ├── if usingTwoDayRule → actualStreak = 0
  │     │     │           └── else → usingTwoDayRule = true
  │     │     │     └── else → actualStreak = 0
  │     │     │
  │     │     ├── DayType.skip:
  │     │     │     ├── skips++
  │     │     │     └── if usingTwoDayRule → actualStreak = 0
  │     │     │
  │     │     └── 记录到 monthlyTracking:
  │     │           └── key = year * 100 + month
  │     │           └── monthlyTracking[key][dayType].add(day)
  │     │
  │     └── 添加到 allStatistics
  │
  └── 4. 汇总 OverallStatisticsData:
        ├── totalChecks = sum(各习惯 checks)
        ├── totalFails = sum(各习惯 fails)
        ├── totalSkips = sum(各习惯 skips)
        └── totalProgress = sum(各习惯 progress)

4. 习惯 CRUD 交互流程

4.1 创建习惯

用户点击 FAB (+)
  │
  └── AppStateManager.goCreateHabit(true)
        │
        └── EditHabitScreen(isNew: true)
              │
              ├── 表单字段:
              │     ├── title (必填, 不能为空)
              │     ├── habitType (下拉: Checkable / Progressive)
              │     │     └── 如果 Progressive:
              │     │           ├── targetValue (NumberFormat('#.##'))
              │     │           ├── partialValue
              │     │           └── unit
              │     ├── twoDayRule (Checkbox)
              │     ├── categories (多选)
              │     ├── notification (Checkbox)
              │     │     └── notTime (TimePicker)
              │     └── [展开] Advanced:
              │           ├── cue (提示触发器)
              │           ├── routine (例行动作)
              │           ├── reward (奖励)
              │           ├── showReward (显示奖励通知)
              │           ├── sanction (惩罚)
              │           ├── showSanction (显示惩罚通知)
              │           └── accountant (问责伙伴)
              │
              └── 点击保存 (FAB ✓):
                    ├── 验证 title 非空
                    ├── 创建 HabitData:
                    │     ├── id: null (数据库自增)
                    │     ├── position: activeHabits.length (追加到末尾)
                    │     └── ... 各字段
                    ├── HabitsManager.addHabit(habit)
                    │     ├── HabitRepository.createHabit() → INSERT
                    │     ├── CategoryRepository.updateHabitCategories()
                    │     ├── 如果 notification → NotificationService.reset()
                    │     └── notifyListeners()
                    └── 导航回主页面

4.2 编辑习惯

用户点击习惯卡片标题区域
  │
  └── AppStateManager.goEditHabit(true)
        │
        └── EditHabitScreen(isNew: false, habit: currentHabit)
              │
              ├── 表单预填充现有数据
              ├── 额外按钮:
              │     ├── 归档/取消归档 (FAB 左侧)
              │     └── 删除 (AppBar)
              │           └── 直接删除,无确认对话框
              │
              └── 点击保存:
                    ├── 更新 HabitData 字段
                    ├── HabitsManager.editHabit(habit)
                    │     ├── HabitRepository.updateHabit() → UPDATE
                    │     ├── CategoryRepository.updateHabitCategories()
                    │     ├── NotificationService.reset()
                    │     └── notifyListeners()
                    └── 导航回主页面

4.3 删除习惯

用户在编辑页点击删除按钮
  │
  └── HabitsManager.deleteHabit(id)
        ├── 从内存列表中移除
        ├── HabitRepository.deleteHabit() → DELETE FROM habits WHERE id = ?
        │     └── CASCADE DELETE events, habit_categories
        ├── NotificationService.removeNotifications(id)
        ├── HomeWidgetService.update()
        ├── UIFeedbackService.showMessageWithAction(
        │     "Habit deleted.",
        │     "Undo",
        │     () => undoDelete()
        │   )
        └── notifyListeners()

4.4 归档/取消归档

归档:
  HabitsManager.archiveHabit(id)
    ├── 更新 habit.archived = true
    ├── HabitRepository.updateHabit()
    ├── NotificationService.removeNotifications(id)
    ├── UIFeedbackService.showSuccess("Habit archived")
    └── notifyListeners()

取消归档:
  HabitsManager.unarchiveHabit(id)
    ├── 更新 habit.archived = false
    ├── HabitRepository.updateHabit()
    ├── 如果 habit.notification → NotificationService.reset()
    ├── UIFeedbackService.showSuccess("Habit unarchived")
    └── notifyListeners()

4.5 拖拽排序

用户长按拖动习惯卡片到新位置
  │
  └── onReorder(oldIndex, newIndex)
        ├── 调整 newIndex (如果 oldIndex < newIndex → newIndex--)
        ├── 列表操作: list.removeAt(oldIndex), list.insert(newIndex, item)
        ├── 更新所有习惯的 position 字段
        ├── HabitsManager.reorderList(oldIndex, newIndex)
        │     ├── 更新内存列表
        │     ├── HabitRepository.updateHabitsOrder() → UPDATE position
        │     └── notifyListeners()
        └── UI 自动刷新

5. 数值型习惯进度输入

5.1 ProgressInputModal 交互

弹出 ProgressInputModal
  │
  ├── 显示:
  │     ├── 标题: "Save Progress"
  │     ├── 圆形进度指示器 (120px 直径)
  │     │     ├── 未完成: 显示百分比 (如 "70%")
  │     │     ├── 已完成: 显示 ✓ 图标
  │     │     └── 超出: 显示百分比 (> 100%)
  │     ├── 当前值 / 目标值 显示
  │     └── 控制按钮行:
  │           ├── [-] 减少 partialValue
  │           ├── [+] 增加 partialValue
  │           └── [Complete] 直接设为 targetValue
  │
  ├── 用户操作:
  │     ├── 点击 [+]:
  │     │     ├── currentValue += partialValue
  │     │     └── clamp(0, targetValue * 2)
  │     ├── 点击 [-]:
  │     │     ├── currentValue -= partialValue
  │     │     └── clamp(0, targetValue * 2)
  │     ├── 点击 Complete:
  │     │     └── currentValue = targetValue
  │     ├── 点击当前值:
  │     │     └── 弹出文本输入框直接编辑
  │     └── 点击 Save:
  │           ├── 确定 DayType:
  │           │     ├── currentValue >= targetValue → DayType.check
  │           │     └── currentValue < targetValue → DayType.progress
  │           ├── 创建事件: [DayType, "", currentValue, targetValue]
  │           └── 返回事件数据给调用方
  │
  └── 点击 Cancel → 返回 null

6. 日历组件行为

6.1 日历格式

月视图 → 显示整月
周视图 → 显示一周

切换触发:
  ├── 用户点击日历标题区域 → 切换格式
  └── 月份切换时重置为月视图

6.2 日历日期标记

每个日期根据当天的 event[0] 显示不同颜色的圆点:

DayType.check     → checkColor (默认 #09BF30 绿色)
DayType.fail      → failColor (默认 #F44336 红色)
DayType.skip      → skipColor (默认 #FBC02D 黄色)
DayType.progress  → progressColor (默认 #2196F3 蓝色)
DayType.clear     → 无标记

今天特殊显示:
  → 外圈高亮环

6.3 月份名称显示

如果 showMonthName 设置为 true:
  → 在日历上方显示当前月份名称文本
如果 false:
  → 不显示

7. 设置项完整规格

7.1 所有设置项及默认值

设置项 类型 默认值 持久化 Key
theme Themes 枚举 Themes.device theme
weekStart StartingDayOfWeek monday weekStart
showDailyNot bool true showDailyNot
dailyNotTime TimeOfDay 20:00 dailyNotTime
soundEffects bool true soundEffects
soundVolume double 3.0 (范围 0-5) soundVolume
biometricLock bool false biometricLock
oneTapCheck bool false oneTapCheck
showMonthName bool true showMonthName
showCategories bool true showCategories
seenOnboarding bool false seenOnboarding
lastWhatsNewVersion String '' lastWhatsNewVersion
checkColor Color #09BF30 checkColor
failColor Color #F44336 failColor
skipColor Color #FBC02D skipColor
progressColor Color #2196F3 progressColor

7.2 设置项分组 (UI 展示顺序)

外观 (Appearance):
  ├── Theme (下拉: Device / Light / Dark / OLED / Material You)
  ├── First day of the week (下拉: Su Mo Tu We Th Fr Sa)
  ├── Show month name (开关)
  ├── Show categories (开关)
  └── Set colors (点击打开颜色选择器 × 4)

通知 (Notifications):
  ├── App notifications (开关) → 控制每日提醒
  └── Notification time (时间选择器, 仅通知开启时可用)

音效 (Sound):
  └── Sound effects (滑块 0-5, 左侧图标, 右侧数字)

安全 (Security):
  ├── Biometric Lock (开关, 需设备支持)
  └── Single tap to check (开关)

数据管理:
  ├── Backup → Create (导出 JSON 文件)
  ├── Backup → Restore (导入 JSON 文件)
  ├── Onboarding (重播引导)
  └── What's New (查看更新日志)

关于:
  ├── App 名称 + 版本号
  ├── Terms and Conditions (URL)
  ├── Privacy Policy (URL)
  ├── Source code (GitHub URL)
  └── Support (捐赠 URL)

8. 备份/恢复流程

8.1 创建备份

用户点击 "Create" 备份按钮
  │
  └── BackupService.createDatabaseBackup()
        │
        ├── 1. 从 HabitsManager 获取所有习惯
        ├── 2. 序列化每个习惯:
        │     ├── id, position, title, twoDayRule, cue, routine, reward
        │     ├── showReward, advanced, notification, notTime
        │     ├── sanction, showSanction, accountant
        │     ├── habitType, targetValue, partialValue, unit
        │     └── events: { "YYYY-MM-DD": [dayTypeIndex, ...] }
        ├── 3. 序列化分类:
        │     └── categories: [{ id, title, iconCodePoint, fontFamily }]
        ├── 4. 序列化关联:
        │     └── habit_categories: [{ habit_id, category_id }]
        ├── 5. 添加元数据:
        │     └── metadata: { import_timestamp, version }
        ├── 6. JSON.encode → 字符串
        ├── 7. 弹出文件保存对话框 (flutter_file_dialog / file_picker)
        └── 8. 写入文件
              ├── 成功 → UIFeedbackService.showSuccess("Backup created successfully!")
              └── 失败 → UIFeedbackService.showError("Backup failed!")

8.2 恢复备份

用户点击 "Restore" 备份按钮
  │
  └── 弹出确认对话框: "All habits will be replaced with habits from backup."
        │
        ├── Cancel → 取消
        │
        └── Restore →
              │
              ├── BackupService.loadBackup()
              │     │
              │     ├── 1. 弹出文件选择对话框
              │     ├── 2. 读取文件内容
              │     ├── 3. 验证:
              │     │     ├── 文件是否存在
              │     │     ├── 文件大小 <= 10MB
              │     │     └── JSON 格式是否有效
              │     ├── 4. 解析 JSON:
              │     │     ├── 如果是数组 → 旧版格式,转换为新格式
              │     │     └── 如果是对象 → 新版格式
              │     ├── 5. 清空数据库:
              │     │     ├── DELETE FROM habits
              │     │     └── DELETE FROM events
              │     ├── 6. 逐个插入习惯和事件
              │     └── 7. 插入分类和关联
              │
              └── 成功后:
                    ├── HabitsManager.loadHabits()  ← 重新加载
                    ├── NotificationService.reset() ← 重置通知
                    ├── HomeWidgetService.update()  ← 更新小组件
                    └── UIFeedbackService.showSuccess("Restore completed successfully!")

9. 引导页流程

首次启动应用
  │
  └── SettingsManager.seenOnboarding == false
        │
        └── 显示 OnboardingScreen
              │
              ├── Step 1: "Define your habits"
              │     ├── 插图: empty_list.svg
              │     ├── 描述: "To better stick to your habits, you can define:"
              │     ├── 概念 1: Cue (提示触发器)
              │     ├── 概念 2: Routine (例行动作)
              │     └── 概念 3: Reward (奖励)
              │
              ├── Step 2: "Log your days"
              │     ├── 插图: habit_tracking.svg
              │     └── 每日操作:
              │           ├── ✓ Successful (完成)
              │           ├── + Progressive (进度)
              │           ├── ✗ Not so successful (失败)
              │           ├── ⏭ Skip (跳过)
              │           └── 💬 Note (备注)
              │
              ├── Step 3: "Observe your progress"
              │     ├── 插图: progress.svg
              │     └── 描述: "You can track your progress through the calendar
              │               view in every habit or on the statistics page."
              │
              ├── 导航: Skip (跳过) / Next (下一步) / Done (完成)
              │
              └── 完成:
                    ├── SettingsManager.seenOnboarding = true
                    ├── SettingsManager.saveData()
                    └── 导航到 HabitsScreen

10. 生物识别认证流程

应用启动 或 从后台恢复 (AppLifecycleState.resumed)
  │
  └── BiometricAuthWrapper
        │
        ├── 检查 biometricLock 设置
        │     ├── false → 直接显示内容
        │     └── true →
        │           │
        │           ├── BiometricAuthService.authenticate()
        │           │     ├── 获取可用生物识别类型
        │           │     │     ├── 指纹 → "Fingerprint"
        │           │     │     ├── 面容 → "Face ID"
        │           │     │     ├── 虹膜 → "Iris"
        │           │     │     └── 设备凭据 → "Device PIN, Pattern, or Password"
        │           │     │
        │           │     └── local_auth.authenticate(
        │           │           localizedReason: "Please authenticate to access Habo",
        │           │           biometricOnly: false
        │           │         )
        │           │
        │           ├── 成功 → 显示内容
        │           │
        │           └── 失败 → 显示认证错误界面:
        │                 ├── 标题: "Authentication Required"
        │                 ├── 描述: "Please authenticate to access Habo"
        │                 ├── 图标: 指纹图标
        │                 └── "Try Again" 按钮 → 重新认证
        │
        └── 设备不支持生物识别:
              ├── showToast: "Please set up fingerprint/face unlock"
              └── 自动关闭 biometricLock 设置

11. 桌面小组件数据更新

习惯事件变化时
  │
  └── HomeWidgetService.update()
        │
        ├── 1. 计算今日习惯进度:
        │     ├── 获取所有活跃习惯
        │     ├── 计算今日已完成数 (DayType.check 或 progress >= target)
        │     └── 计算总习惯数
        │
        ├── 2. 写入小组件数据:
        │     ├── HomeWidget.saveWidgetData<int>("habitsCompleted", count)
        │     ├── HomeWidget.saveWidgetData<int>("habitsTotal", total)
        │     └── HomeWidget.updateWidget()
        │
        └── 3. 小组件渲染:
              ├── CircularProgressPainter
              │     └── 弧度 = (completed / total) * 2π
              ├── 中心显示: "completed / total"
              └── 尺寸: 170 × 170

12. 通知调度逻辑

NotificationService.resetNotifications():
  │
  ├── 1. 取消所有现有通知:
  │     └── AwesomeNotifications().cancelAll()
  │
  ├── 2. 检查全局通知开关:
  │     └── if !showDailyNot → return
  │
  └── 3. 遍历所有活跃习惯:
        └── if habit.notification:
              ├── 创建每日重复通知:
              │     ├── id: habit.id
              │     ├── channel: "habit_notifications"
              │     ├── title: "Habo"
              │     ├── body: "Do not forget to check your habits."
              │     ├── schedule: 每日 at habit.notTime
              │     └── payload: { habitId: habit.id }
              │
              └── AwesomeNotifications().createNotification()

13. 完整数据库 Schema

13.1 habits 表

CREATE TABLE habits (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    position INTEGER,                    -- 排序权重
    title TEXT NOT NULL,                 -- 习惯标题 (必填)
    twoDayRule INTEGER DEFAULT 0,        -- 两天法则 0=关 1=开
    cue TEXT DEFAULT '',                 -- 提示触发器
    routine TEXT DEFAULT '',             -- 例行动作
    reward TEXT DEFAULT '',              -- 奖励
    showReward INTEGER DEFAULT 0,        -- 显示奖励 0=关 1=开
    advanced INTEGER DEFAULT 0,          -- 高级模式 0=关 1=开
    notification INTEGER DEFAULT 0,      -- 通知开关 0=关 1=开
    notTime TEXT DEFAULT '',             -- 通知时间 "HH:MM"
    sanction TEXT DEFAULT '',            -- 惩罚描述
    showSanction INTEGER DEFAULT 0,      -- 显示惩罚 0=关 1=开
    accountant TEXT DEFAULT '',          -- 问责伙伴
    habitType INTEGER DEFAULT 0,         -- 0=布尔 1=数值
    targetValue REAL DEFAULT 1.0,        -- 目标值
    partialValue REAL DEFAULT 1.0,       -- 部分增量
    unit TEXT DEFAULT '',                -- 单位
    archived INTEGER DEFAULT 0           -- 0=活跃 1=归档
);

13.2 events 表

CREATE TABLE events (
    id INTEGER NOT NULL,                 -- FK → habits.id
    dateTime TEXT NOT NULL,              -- ISO8601 日期字符串
    dayType INTEGER NOT NULL,            -- 0=clear 1=check 2=fail 3=skip 4=progress
    comment TEXT DEFAULT '',             -- 备注
    progressValue REAL DEFAULT 0.0,      -- 进度值 (数值型)
    targetValue REAL DEFAULT 0.0,        -- 目标值快照 (数值型)
    PRIMARY KEY (id, dateTime),
    FOREIGN KEY (id) REFERENCES habits(id) ON DELETE CASCADE
);

13.3 categories 表

CREATE TABLE categories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,                 -- 分类名称
    iconCodePoint INTEGER NOT NULL,      -- IconData.codePoint
    fontFamily TEXT                      -- 字体族 (如 fontAwesomeFlutter)
);

13.4 habit_categories 关联表

CREATE TABLE habit_categories (
    habit_id INTEGER NOT NULL,
    category_id INTEGER NOT NULL,
    PRIMARY KEY (habit_id, category_id),
    FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE,
    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);

14. 跨日自动刷新

应用启动时:
  └── _startDayChangeTimer()
        └── Timer.periodic(Duration(hours: 1), callback)
              │
              ├── 记录当前日期: lastDate = DateTime.now().day
              │
              └── 每小时检查:
                    ├── if DateTime.now().day != lastDate:
                    │     ├── lastDate = DateTime.now().day
                    │     ├── HabitsManager.loadHabits()     ← 重载数据
                    │     ├── NotificationService.reset()    ← 重置通知
                    │     └── HomeWidgetService.update()     ← 更新小组件
                    │
                    └── else → 无操作

应用暂停时:
  └── _stopDayChangeTimer()  ← 暂停定时器

应用恢复时:
  └── 检查日期变化 → 刷新 → _startDayChangeTimer()  ← 恢复定时器

15. 颜色选择器交互

用户点击颜色设置项
  │
  └── 弹出 ColorIcon 对话框
        │
        ├── 显示 HueRingPicker:
        │     ├── 色相环 (360°)
        │     └── 饱和度/亮度选择区域
        │
        ├── 当前选中颜色的实时预览
        ├── 重置按钮 → 恢复默认颜色
        │
        └── 确认 → SettingsManager 更新颜色值
              ├── SharedPreferences 保存 (ARGB int)
              └── notifyListeners() → 全局主题刷新