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

808 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 事件数据结构
```dart
// 布尔型习惯
[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 数据结构
```dart
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 表
```sql
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 表
```sql
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 表
```sql
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 关联表
```sql
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() → 全局主题刷新
```