feat(app): BLoC 集成 Repository + SettingsBloc 主题切换
全局依赖注入: - app.dart 注入 JournalRepository + ClassRepository + SettingsBloc - ApiClient token 自动注入(监听 AuthBloc 状态) BLoC 重构 (占位数据 → Repository): - CalendarBloc: 通过 JournalRepository 加载月度日记 - ClassBloc: 通过 ClassRepository + JournalRepository 加载班级数据 - 新增 ClassJoin 事件支持班级码加入 - HomeBloc: 加载最近日记 + 心情概览 + 连续天数 + 今日是否已写 设置系统: - SettingsBloc: ThemeMode 切换 (system/light/dark) - app.dart 通过 ListenableBuilder 响应主题变化 - HomeBloc 支持下拉刷新 首页增强: - 连续天数徽章 + 今日已写标记 + 最常用心情高亮 - RefreshIndicator 下拉刷新 - 日记列表卡片显示日期 验证: flutter analyze 0 error
This commit is contained in:
139
app/lib/features/home/bloc/home_bloc.dart
Normal file
139
app/lib/features/home/bloc/home_bloc.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// 首页 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;
|
||||
|
||||
const HomeLoaded({
|
||||
this.recentJournals = const [],
|
||||
this.hasTodayEntry = false,
|
||||
this.topMood,
|
||||
this.streakDays = 0,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
emit(HomeLoaded(
|
||||
recentJournals: journals,
|
||||
hasTodayEntry: hasTodayEntry,
|
||||
topMood: topMood,
|
||||
streakDays: streakDays,
|
||||
));
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user