- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync - fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local' - fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint - feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter) - feat(editor): EditorBloc 扩展 + EditorPage 增强 - feat(search): SearchBloc 扩展搜索功能 - feat(home): HomeBloc/HomePage 增强 - feat(auth): LoginPage 增强 - feat(templates): TemplateGalleryPage 重构 - fix(web): 管理端班级/日记页面修复 - fix(server): comment_service + theme_handler 修复 - docs: 添加全链路审计报告和验证截图
171 lines
4.4 KiB
Dart
171 lines
4.4 KiB
Dart
// 首页 BLoC — 加载最近日记和心情概览
|
||
|
||
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);
|
||
|
||
// 本月日记数(spec §3.4 quick-stats)
|
||
final monthCount = journals.where((j) =>
|
||
j.date.year == today.year && j.date.month == today.month).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) {
|
||
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;
|
||
}
|
||
}
|