- 8b-D01: Isar 添加 authorId+dateEpoch 复合索引和 dateEpoch 单独索引 - 8b-D02: getJournals 分页改为 DB 层 .offset().limit() 替代 Dart 层 sublist - 8b-D03: home_bloc monthCount 改用日期范围独立查询(不受分页限制) - 8b-M02: 笔画光栅化改为 BBox 裁剪 — 短笔画不再创建全画布尺寸图像 - _CacheEntry 增加 offset 字段记录 BBox 偏移 - _rasterizeStroke 计算包围盒 + 4px padding - _compositeIncremental 使用 offset 定位 - 8b-N01: SyncEngine enqueue 合并同一资源的操作 - create+update → create(最新数据) - update+update → update(最新数据) - update+delete → delete - create+delete → 取消(不发送) - 注意: Isar .g.dart 需运行 build_runner 重新生成
178 lines
4.7 KiB
Dart
178 lines
4.7 KiB
Dart
// 首页 BLoC — 加载最近日记和心情概览
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
||
|
||
// ===== Events =====
|
||
|
||
sealed class HomeEvent {
|
||
const HomeEvent();
|
||
}
|
||
|
||
/// 加载首页数据(最近日记 + 心情概览)
|
||
final class HomeLoadData extends HomeEvent {
|
||
const HomeLoadData();
|
||
}
|
||
|
||
/// 刷新首页
|
||
final class HomeRefresh extends HomeEvent {
|
||
const HomeRefresh();
|
||
}
|
||
|
||
// ===== State =====
|
||
|
||
/// 首页状态
|
||
sealed class HomeState {
|
||
const HomeState();
|
||
}
|
||
|
||
final class HomeInitial extends HomeState {
|
||
const HomeInitial();
|
||
}
|
||
|
||
final class HomeLoading extends HomeState {
|
||
const HomeLoading();
|
||
}
|
||
|
||
final class HomeLoaded extends HomeState {
|
||
/// 最近日记(取最新 10 条)
|
||
final List<JournalEntry> recentJournals;
|
||
|
||
/// 今日是否已写日记
|
||
final bool hasTodayEntry;
|
||
|
||
/// 最近常用心情
|
||
final Mood? topMood;
|
||
|
||
/// 连续写日记天数(从日记列表推算)
|
||
final int streakDays;
|
||
|
||
/// 本月日记数(spec §3.4 quick-stats)
|
||
final int monthCount;
|
||
|
||
/// 总日记数(spec §3.4 quick-stats)
|
||
final int totalCount;
|
||
|
||
/// 今日天气(从今日日记中提取,null 则默认晴)
|
||
final Weather? todayWeather;
|
||
|
||
const HomeLoaded({
|
||
this.recentJournals = const [],
|
||
this.hasTodayEntry = false,
|
||
this.topMood,
|
||
this.streakDays = 0,
|
||
this.monthCount = 0,
|
||
this.totalCount = 0,
|
||
this.todayWeather,
|
||
});
|
||
}
|
||
|
||
final class HomeError extends HomeState {
|
||
final String message;
|
||
const HomeError(this.message);
|
||
}
|
||
|
||
// ===== BLoC =====
|
||
|
||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||
final JournalRepository _journalRepo;
|
||
|
||
HomeBloc({required JournalRepository journalRepository})
|
||
: _journalRepo = journalRepository,
|
||
super(const HomeInitial()) {
|
||
on<HomeLoadData>(_onLoadData);
|
||
on<HomeRefresh>(_onRefresh);
|
||
}
|
||
|
||
Future<void> _onLoadData(
|
||
HomeLoadData event,
|
||
Emitter<HomeState> emit,
|
||
) async {
|
||
emit(const HomeLoading());
|
||
try {
|
||
final journals = await _journalRepo.getJournals(
|
||
page: 1,
|
||
pageSize: 10,
|
||
);
|
||
|
||
// 检查今日是否已写日记
|
||
final today = DateTime.now();
|
||
final hasTodayEntry = journals.any((j) =>
|
||
j.date.year == today.year &&
|
||
j.date.month == today.month &&
|
||
j.date.day == today.day);
|
||
|
||
// 推算最常用心情
|
||
final moodCounts = <Mood, int>{};
|
||
for (final j in journals) {
|
||
moodCounts[j.mood] = (moodCounts[j.mood] ?? 0) + 1;
|
||
}
|
||
final topMood = moodCounts.entries
|
||
.fold<MapEntry<Mood, int>?>(null, (a, b) => a == null || b.value > a.value ? b : a)
|
||
?.key;
|
||
|
||
// 推算连续天数
|
||
final streakDays = _calculateStreak(journals);
|
||
|
||
// 本月日记数 — 使用日期范围查询,不受分页限制(修复 8b-D03)
|
||
final monthStart = DateTime(today.year, today.month, 1);
|
||
final monthEnd = DateTime(today.year, today.month + 1, 1);
|
||
final monthJournals = await _journalRepo.getJournals(
|
||
dateFrom: monthStart,
|
||
dateTo: monthEnd,
|
||
);
|
||
final monthCount = monthJournals.length;
|
||
|
||
// 总日记数 — 使用仓库计数方法(不受分页限制)
|
||
final totalCount = await _journalRepo.getJournalCount();
|
||
|
||
// 今日天气 — 从今日日记中提取
|
||
final todayJournal = journals.firstWhere(
|
||
(j) => j.date.year == today.year &&
|
||
j.date.month == today.month &&
|
||
j.date.day == today.day,
|
||
orElse: () => journals.first, // fallback for type safety
|
||
);
|
||
final todayWeather = hasTodayEntry ? todayJournal.weather : null;
|
||
|
||
emit(HomeLoaded(
|
||
recentJournals: journals,
|
||
hasTodayEntry: hasTodayEntry,
|
||
topMood: topMood,
|
||
streakDays: streakDays,
|
||
monthCount: monthCount,
|
||
totalCount: totalCount,
|
||
todayWeather: todayWeather,
|
||
));
|
||
} catch (e) {
|
||
debugPrint('HomeBloc._onLoadData 失败: $e');
|
||
emit(const HomeLoaded()); // 空状态而非错误,离线友好
|
||
}
|
||
}
|
||
|
||
Future<void> _onRefresh(
|
||
HomeRefresh event,
|
||
Emitter<HomeState> emit,
|
||
) async {
|
||
add(const HomeLoadData());
|
||
}
|
||
|
||
/// 从日记列表推算连续写日记天数
|
||
int _calculateStreak(List<JournalEntry> journals) {
|
||
if (journals.isEmpty) return 0;
|
||
|
||
final dates = journals.map((j) => j.date).toSet();
|
||
var streak = 0;
|
||
var checkDate = DateTime.now();
|
||
|
||
while (dates.contains(DateTime(checkDate.year, checkDate.month, checkDate.day))) {
|
||
streak++;
|
||
checkDate = checkDate.subtract(const Duration(days: 1));
|
||
}
|
||
|
||
return streak;
|
||
}
|
||
}
|