Files
nj/app/lib/features/calendar/bloc/calendar_bloc.dart
iven 7e3597dc77 feat(diary): B4+B5+B6 后端服务 + F5/F6/F7 前端模块
后端 (erp-diary):
- B4: CommentService 班级成员验证 + 删除评语 + SSE 通知推送
- B4: NotificationService 评语/主题/成就三类通知事件
- B5: StickerService 贴纸包列表 + 贴纸查询 + 模板管理
- B5: AchievementService 成就列表 + 解锁 + SSE 通知
- B6: MoodStatsService 心情统计 + 连续天数
- B6: ContentSafetyService 敏感词过滤框架
- SSE handler 增加 diary.notification.* 事件处理
- 新增 14 个 API 端点 + diary.comment.delete 权限

前端 (Flutter):
- F5: CalendarBloc + 月视图日历 + 日记列表
- F6: MoodBloc + fl_chart 心情饼图 + 统计卡片 + 连续天数
- F7: 贴纸库分类浏览 + 模板画廊
- 首页改为日记流 + 心情快速选择
- 成就页改为徽章收集展示

验证: cargo check ✓ cargo test 17/17 ✓ flutter analyze 0 error
2026-06-01 09:32:09 +08:00

176 lines
4.7 KiB
Dart

// 日历 BLoC — 管理日历视图状态和日记列表
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nuanji_app/data/models/journal_entry.dart';
// ===== Events =====
sealed class CalendarEvent {
const CalendarEvent();
}
/// 切换到指定月份
final class CalendarMonthChanged extends CalendarEvent {
final DateTime month;
const CalendarMonthChanged(this.month);
}
/// 选择某一天
final class CalendarDaySelected extends CalendarEvent {
final DateTime day;
const CalendarDaySelected(this.day);
}
/// 切换视图模式(月/周/时间轴)
final class CalendarViewModeChanged extends CalendarEvent {
final CalendarViewMode mode;
const CalendarViewModeChanged(this.mode);
}
/// 加载某月的日记列表
final class CalendarLoadJournals extends CalendarEvent {
final DateTime month;
const CalendarLoadJournals(this.month);
}
// ===== State =====
/// 日历视图模式
enum CalendarViewMode { month, week, timeline }
/// 日历状态
sealed class CalendarState {
const CalendarState();
}
/// 初始加载中
final class CalendarInitial extends CalendarState {
const CalendarInitial();
}
/// 日历已加载 — 包含当前月份、选中日期、日记列表
final class CalendarLoaded extends CalendarState {
/// 当前显示的月份
final DateTime focusedMonth;
/// 选中的日期
final DateTime selectedDay;
/// 当前月份所有日记(按日期索引)
final Map<DateTime, List<JournalEntry>> journalsByDate;
/// 当前选中日期的日记列表
final List<JournalEntry> selectedDayJournals;
/// 视图模式
final CalendarViewMode viewMode;
/// 是否正在加载
final bool isLoading;
const CalendarLoaded({
required this.focusedMonth,
required this.selectedDay,
required this.journalsByDate,
required this.selectedDayJournals,
this.viewMode = CalendarViewMode.month,
this.isLoading = false,
});
CalendarLoaded copyWith({
DateTime? focusedMonth,
DateTime? selectedDay,
Map<DateTime, List<JournalEntry>>? journalsByDate,
List<JournalEntry>? selectedDayJournals,
CalendarViewMode? viewMode,
bool? isLoading,
}) =>
CalendarLoaded(
focusedMonth: focusedMonth ?? this.focusedMonth,
selectedDay: selectedDay ?? this.selectedDay,
journalsByDate: journalsByDate ?? this.journalsByDate,
selectedDayJournals: selectedDayJournals ?? this.selectedDayJournals,
viewMode: viewMode ?? this.viewMode,
isLoading: isLoading ?? this.isLoading,
);
}
/// 加载失败
final class CalendarError extends CalendarState {
final String message;
const CalendarError(this.message);
}
// ===== BLoC =====
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
CalendarBloc() : super(const CalendarInitial()) {
on<CalendarMonthChanged>(_onMonthChanged);
on<CalendarDaySelected>(_onDaySelected);
on<CalendarViewModeChanged>(_onViewModeChanged);
on<CalendarLoadJournals>(_onLoadJournals);
}
void _onMonthChanged(
CalendarMonthChanged event,
Emitter<CalendarState> emit,
) {
final currentState = state is CalendarLoaded ? state as CalendarLoaded : null;
emit(CalendarLoaded(
focusedMonth: event.month,
selectedDay: event.month,
journalsByDate: currentState?.journalsByDate ?? {},
selectedDayJournals: [],
viewMode: currentState?.viewMode ?? CalendarViewMode.month,
));
add(CalendarLoadJournals(event.month));
}
void _onDaySelected(
CalendarDaySelected event,
Emitter<CalendarState> emit,
) {
if (state is! CalendarLoaded) return;
final current = state as CalendarLoaded;
// 查找选中日期的日记
final dayKey = DateTime(event.day.year, event.day.month, event.day.day);
final dayJournals = current.journalsByDate[dayKey] ?? [];
emit(current.copyWith(
selectedDay: event.day,
selectedDayJournals: dayJournals,
));
}
void _onViewModeChanged(
CalendarViewModeChanged event,
Emitter<CalendarState> emit,
) {
if (state is! CalendarLoaded) return;
final current = state as CalendarLoaded;
emit(current.copyWith(viewMode: event.mode));
}
Future<void> _onLoadJournals(
CalendarLoadJournals event,
Emitter<CalendarState> emit,
) async {
if (state is! CalendarLoaded) return;
final current = state as CalendarLoaded;
emit(current.copyWith(isLoading: true));
// Phase 1: 使用空数据占位,待 Repository 集成后替换
// 实际将从 JournalRepository.loadByMonth(event.month) 获取
await Future.delayed(const Duration(milliseconds: 300));
emit(current.copyWith(
isLoading: false,
journalsByDate: current.journalsByDate,
));
}
}