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:
iven
2026-06-01 10:32:20 +08:00
parent 263ddf31a6
commit 860e9e5d22
9 changed files with 630 additions and 315 deletions

View File

@@ -1,8 +1,10 @@
// 班级 BLoC — 管理班级状态、成员、日记墙、主题布置和评语
// 班级 BLoC — 通过 ClassRepository 管理班级数据
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nuanji_app/data/models/journal_entry.dart';
import 'package:nuanji_app/data/models/school_class.dart';
import 'package:nuanji_app/data/repositories/class_repository.dart';
import 'package:nuanji_app/data/repositories/journal_repository.dart';
// ===== Events =====
@@ -10,43 +12,41 @@ sealed class ClassEvent {
const ClassEvent();
}
/// 加载我的班级列表
final class ClassLoadMyClasses extends ClassEvent {
const ClassLoadMyClasses();
}
/// 选择当前班级
final class ClassSelected extends ClassEvent {
final String classId;
const ClassSelected(this.classId);
}
/// 加载班级成员列表
final class ClassLoadMembers extends ClassEvent {
final String classId;
const ClassLoadMembers(this.classId);
}
/// 加载班级日记墙(已分享到班级的日记)
final class ClassLoadDiaryWall extends ClassEvent {
final String classId;
const ClassLoadDiaryWall(this.classId);
}
/// 加载主题布置列表
final class ClassLoadTopics extends ClassEvent {
final String classId;
const ClassLoadTopics(this.classId);
}
/// 创建班级(老师)
final class ClassLoadComments extends ClassEvent {
final String journalId;
const ClassLoadComments(this.journalId);
}
final class ClassCreate extends ClassEvent {
final String name;
final String? schoolName;
const ClassCreate({required this.name, this.schoolName});
}
/// 布置主题(老师)
final class TopicAssign extends ClassEvent {
final String classId;
final String title;
@@ -60,30 +60,22 @@ final class TopicAssign extends ClassEvent {
});
}
/// 加载日记评语
final class ClassLoadComments extends ClassEvent {
final String journalId;
const ClassLoadComments(this.journalId);
final class ClassJoin extends ClassEvent {
final String classCode;
final String? nickname;
const ClassJoin({required this.classCode, this.nickname});
}
// ===== State =====
/// 班级成员模型
class ClassMember {
final String userId;
final String role;
final String? nickname;
final DateTime joinedAt;
const ClassMember({
required this.userId,
required this.role,
this.nickname,
required this.joinedAt,
});
const ClassMember({required this.userId, required this.role, this.nickname, required this.joinedAt});
}
/// 主题布置模型
class TopicAssignment {
final String id;
final String classId;
@@ -92,71 +84,38 @@ class TopicAssignment {
final String? description;
final DateTime? dueDate;
final bool isActive;
const TopicAssignment({
required this.id,
required this.classId,
required this.teacherId,
required this.title,
this.description,
this.dueDate,
this.isActive = true,
});
const TopicAssignment({required this.id, required this.classId, required this.teacherId, required this.title, this.description, this.dueDate, this.isActive = true});
}
/// 评语模型
class Comment {
final String id;
final String journalId;
final String authorId;
final String content;
final DateTime createdAt;
const Comment({
required this.id,
required this.journalId,
required this.authorId,
required this.content,
required this.createdAt,
});
const Comment({required this.id, required this.journalId, required this.authorId, required this.content, required this.createdAt});
}
/// 班级状态
sealed class ClassState {
const ClassState();
}
/// 初始状态
final class ClassInitial extends ClassState {
const ClassInitial();
}
/// 加载中
final class ClassLoading extends ClassState {
const ClassLoading();
}
/// 班级列表已加载
final class ClassListLoaded extends ClassState {
final List<SchoolClass> classes;
final bool isLoading;
const ClassListLoaded({
this.classes = const [],
this.isLoading = false,
});
ClassListLoaded copyWith({
List<SchoolClass>? classes,
bool? isLoading,
}) =>
ClassListLoaded(
classes: classes ?? this.classes,
isLoading: isLoading ?? this.isLoading,
);
const ClassListLoaded({this.classes = const [], this.isLoading = false});
ClassListLoaded copyWith({List<SchoolClass>? classes, bool? isLoading}) =>
ClassListLoaded(classes: classes ?? this.classes, isLoading: isLoading ?? this.isLoading);
}
/// 班级详情已加载(日记墙 / 成员 / 主题 / 评语)
final class ClassDetailLoaded extends ClassState {
final SchoolClass classInfo;
final List<ClassMember> members;
@@ -197,13 +156,10 @@ final class ClassDetailLoaded extends ClassState {
comments: comments ?? this.comments,
isLoadingWall: isLoadingWall ?? this.isLoadingWall,
isLoadingMembers: isLoadingMembers ?? this.isLoadingMembers,
selectedJournalId: clearSelectedJournal
? null
: (selectedJournalId ?? this.selectedJournalId),
selectedJournalId: clearSelectedJournal ? null : (selectedJournalId ?? this.selectedJournalId),
);
}
/// 错误状态
final class ClassError extends ClassState {
final String message;
const ClassError(this.message);
@@ -212,15 +168,24 @@ final class ClassError extends ClassState {
// ===== BLoC =====
class ClassBloc extends Bloc<ClassEvent, ClassState> {
ClassBloc() : super(const ClassInitial()) {
final ClassRepository _classRepo;
final JournalRepository _journalRepo;
ClassBloc({
required ClassRepository classRepository,
required JournalRepository journalRepository,
}) : _classRepo = classRepository,
_journalRepo = journalRepository,
super(const ClassInitial()) {
on<ClassLoadMyClasses>(_onLoadMyClasses);
on<ClassSelected>(_onClassSelected);
on<ClassLoadMembers>(_onLoadMembers);
on<ClassLoadDiaryWall>(_onLoadDiaryWall);
on<ClassLoadTopics>(_onLoadTopics);
on<ClassLoadComments>(_onLoadComments);
on<ClassCreate>(_onCreateClass);
on<TopicAssign>(_onTopicAssign);
on<ClassLoadComments>(_onLoadComments);
on<ClassJoin>(_onJoinClass);
}
Future<void> _onLoadMyClasses(
@@ -228,46 +193,27 @@ class ClassBloc extends Bloc<ClassEvent, ClassState> {
Emitter<ClassState> emit,
) async {
emit(const ClassListLoaded(isLoading: true));
// Phase 1: 占位数据,待 API 集成
await Future.delayed(const Duration(milliseconds: 300));
final now = DateTime.now();
emit(ClassListLoaded(classes: [
SchoolClass(
id: 'class-1',
name: '三年二班',
schoolName: '阳光小学',
teacherId: 'teacher-1',
classCode: 'a1b2c3',
memberCount: 28,
createdAt: now,
updatedAt: now,
),
]));
try {
final classes = await _classRepo.getMyClasses();
emit(ClassListLoaded(classes: classes));
} catch (e) {
emit(ClassListLoaded(classes: const []));
}
}
Future<void> _onClassSelected(
ClassSelected event,
Emitter<ClassState> emit,
) async {
final now = DateTime.now();
final classInfo = SchoolClass(
id: event.classId,
name: '三年二班',
schoolName: '阳光小学',
teacherId: 'teacher-1',
classCode: 'a1b2c3',
memberCount: 28,
createdAt: now,
updatedAt: now,
);
emit(ClassDetailLoaded(classInfo: classInfo));
add(ClassLoadDiaryWall(event.classId));
add(ClassLoadMembers(event.classId));
add(ClassLoadTopics(event.classId));
try {
final classInfo = await _classRepo.getClass(event.classId);
emit(ClassDetailLoaded(classInfo: classInfo));
add(ClassLoadDiaryWall(event.classId));
add(ClassLoadMembers(event.classId));
add(ClassLoadTopics(event.classId));
} catch (e) {
emit(ClassError('加载班级失败: $e'));
}
}
Future<void> _onLoadMembers(
@@ -278,20 +224,20 @@ class ClassBloc extends Bloc<ClassEvent, ClassState> {
final current = state as ClassDetailLoaded;
emit(current.copyWith(isLoadingMembers: true));
await Future.delayed(const Duration(milliseconds: 200));
final now = DateTime.now();
final members = List.generate(
28,
(i) => ClassMember(
userId: 'user-$i',
role: i == 0 ? 'teacher' : 'student',
nickname: i == 0 ? '王老师' : '同学$i',
joinedAt: now,
),
);
emit(current.copyWith(members: members, isLoadingMembers: false));
try {
final dtos = await _classRepo.getMembers(event.classId);
final members = dtos
.map((d) => ClassMember(
userId: d.userId,
role: d.role,
nickname: d.nickname,
joinedAt: d.joinedAt,
))
.toList();
emit(current.copyWith(members: members, isLoadingMembers: false));
} catch (_) {
emit(current.copyWith(isLoadingMembers: false));
}
}
Future<void> _onLoadDiaryWall(
@@ -302,26 +248,17 @@ class ClassBloc extends Bloc<ClassEvent, ClassState> {
final current = state as ClassDetailLoaded;
emit(current.copyWith(isLoadingWall: true));
await Future.delayed(const Duration(milliseconds: 200));
final now = DateTime.now();
final titles = ['快乐的周末', '春天来了', '我的小猫', '数学课', '好朋友', '下雨天'];
try {
// 加载属于该班级的公开日记
final journals = await _journalRepo.getJournals();
final classJournals = journals
.where((j) => j.classId == event.classId && j.sharedToClass)
.toList();
final diaries = List.generate(
6,
(i) => JournalEntry(
id: 'diary-$i',
authorId: 'user-${i + 1}',
classId: event.classId,
title: titles[i],
date: now.subtract(Duration(days: i)),
mood: Mood.values[i % Mood.values.length],
tags: const ['日常', '心情'],
createdAt: now,
updatedAt: now,
),
);
emit(current.copyWith(diaryWall: diaries, isLoadingWall: false));
emit(current.copyWith(diaryWall: classJournals, isLoadingWall: false));
} catch (_) {
emit(current.copyWith(isLoadingWall: false));
}
}
Future<void> _onLoadTopics(
@@ -331,61 +268,25 @@ class ClassBloc extends Bloc<ClassEvent, ClassState> {
if (state is! ClassDetailLoaded) return;
final current = state as ClassDetailLoaded;
final topics = [
TopicAssignment(
id: 'topic-1',
classId: event.classId,
teacherId: 'teacher-1',
title: '我的周末',
description: '写一篇关于你周末生活的日记',
dueDate: DateTime.now().add(const Duration(days: 3)),
),
TopicAssignment(
id: 'topic-2',
classId: event.classId,
teacherId: 'teacher-1',
title: '最开心的一天',
description: '回忆一下让你最开心的一天',
),
];
emit(current.copyWith(topics: topics));
}
Future<void> _onCreateClass(
ClassCreate event,
Emitter<ClassState> emit,
) async {
final newClass = SchoolClass.create(
name: event.name,
schoolName: event.schoolName ?? '',
teacherId: 'current-user',
classCode: 'x1y2z3',
);
if (state is ClassListLoaded) {
final current = state as ClassListLoaded;
emit(current.copyWith(classes: [...current.classes, newClass]));
try {
final dtos = await _classRepo.getTopics(event.classId);
final topics = dtos
.map((d) => TopicAssignment(
id: d.id,
classId: d.classId,
teacherId: d.teacherId,
title: d.title,
description: d.description,
dueDate: d.dueDate,
isActive: d.isActive,
))
.toList();
emit(current.copyWith(topics: topics));
} catch (_) {
// 静默失败,保留空列表
}
}
Future<void> _onTopicAssign(
TopicAssign event,
Emitter<ClassState> emit,
) async {
if (state is! ClassDetailLoaded) return;
final current = state as ClassDetailLoaded;
final newTopic = TopicAssignment(
id: 'topic-${DateTime.now().millisecondsSinceEpoch}',
classId: event.classId,
teacherId: 'current-user',
title: event.title,
description: event.description,
dueDate: event.dueDate,
);
emit(current.copyWith(topics: [newTopic, ...current.topics]));
}
Future<void> _onLoadComments(
ClassLoadComments event,
Emitter<ClassState> emit,
@@ -393,18 +294,79 @@ class ClassBloc extends Bloc<ClassEvent, ClassState> {
if (state is! ClassDetailLoaded) return;
final current = state as ClassDetailLoaded;
final comments = [
Comment(
id: 'comment-1',
journalId: event.journalId,
authorId: 'teacher-1',
content: '写得很好,继续保持!',
createdAt: DateTime.now(),
),
];
emit(current.copyWith(
comments: comments,
selectedJournalId: event.journalId,
));
try {
final dtos = await _classRepo.getComments(event.journalId);
final comments = dtos
.map((d) => Comment(
id: d.id,
journalId: d.journalId,
authorId: d.authorId,
content: d.content,
createdAt: d.createdAt,
))
.toList();
emit(current.copyWith(comments: comments, selectedJournalId: event.journalId));
} catch (_) {
emit(current.copyWith(selectedJournalId: event.journalId));
}
}
Future<void> _onCreateClass(
ClassCreate event,
Emitter<ClassState> emit,
) async {
try {
final newClass = await _classRepo.createClass(
name: event.name,
schoolName: event.schoolName,
);
if (state is ClassListLoaded) {
final current = state as ClassListLoaded;
emit(current.copyWith(classes: [...current.classes, newClass]));
}
} catch (e) {
// 创建失败不改变状态
}
}
Future<void> _onTopicAssign(
TopicAssign event,
Emitter<ClassState> emit,
) async {
if (state is! ClassDetailLoaded) return;
final current = state as ClassDetailLoaded;
try {
final dto = await _classRepo.assignTopic(
classId: event.classId,
title: event.title,
description: event.description,
dueDate: event.dueDate,
);
final newTopic = TopicAssignment(
id: dto.id,
classId: dto.classId,
teacherId: dto.teacherId,
title: dto.title,
description: dto.description,
dueDate: dto.dueDate,
);
emit(current.copyWith(topics: [newTopic, ...current.topics]));
} catch (_) {
// 静默失败
}
}
Future<void> _onJoinClass(
ClassJoin event,
Emitter<ClassState> emit,
) async {
try {
await _classRepo.joinClass(event.classCode, nickname: event.nickname);
// 加入成功后刷新列表
add(const ClassLoadMyClasses());
} catch (e) {
emit(ClassError('加入班级失败: $e'));
}
}
}