全局依赖注入: - 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
373 lines
10 KiB
Dart
373 lines
10 KiB
Dart
// 班级 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 =====
|
|
|
|
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;
|
|
final String? description;
|
|
final DateTime? dueDate;
|
|
const TopicAssign({
|
|
required this.classId,
|
|
required this.title,
|
|
this.description,
|
|
this.dueDate,
|
|
});
|
|
}
|
|
|
|
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});
|
|
}
|
|
|
|
class TopicAssignment {
|
|
final String id;
|
|
final String classId;
|
|
final String teacherId;
|
|
final String title;
|
|
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});
|
|
}
|
|
|
|
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});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
final class ClassDetailLoaded extends ClassState {
|
|
final SchoolClass classInfo;
|
|
final List<ClassMember> members;
|
|
final List<JournalEntry> diaryWall;
|
|
final List<TopicAssignment> topics;
|
|
final List<Comment> comments;
|
|
final bool isLoadingWall;
|
|
final bool isLoadingMembers;
|
|
final String? selectedJournalId;
|
|
|
|
const ClassDetailLoaded({
|
|
required this.classInfo,
|
|
this.members = const [],
|
|
this.diaryWall = const [],
|
|
this.topics = const [],
|
|
this.comments = const [],
|
|
this.isLoadingWall = false,
|
|
this.isLoadingMembers = false,
|
|
this.selectedJournalId,
|
|
});
|
|
|
|
ClassDetailLoaded copyWith({
|
|
SchoolClass? classInfo,
|
|
List<ClassMember>? members,
|
|
List<JournalEntry>? diaryWall,
|
|
List<TopicAssignment>? topics,
|
|
List<Comment>? comments,
|
|
bool? isLoadingWall,
|
|
bool? isLoadingMembers,
|
|
String? selectedJournalId,
|
|
bool clearSelectedJournal = false,
|
|
}) =>
|
|
ClassDetailLoaded(
|
|
classInfo: classInfo ?? this.classInfo,
|
|
members: members ?? this.members,
|
|
diaryWall: diaryWall ?? this.diaryWall,
|
|
topics: topics ?? this.topics,
|
|
comments: comments ?? this.comments,
|
|
isLoadingWall: isLoadingWall ?? this.isLoadingWall,
|
|
isLoadingMembers: isLoadingMembers ?? this.isLoadingMembers,
|
|
selectedJournalId: clearSelectedJournal ? null : (selectedJournalId ?? this.selectedJournalId),
|
|
);
|
|
}
|
|
|
|
final class ClassError extends ClassState {
|
|
final String message;
|
|
const ClassError(this.message);
|
|
}
|
|
|
|
// ===== BLoC =====
|
|
|
|
class ClassBloc extends Bloc<ClassEvent, ClassState> {
|
|
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<ClassJoin>(_onJoinClass);
|
|
}
|
|
|
|
Future<void> _onLoadMyClasses(
|
|
ClassLoadMyClasses event,
|
|
Emitter<ClassState> emit,
|
|
) async {
|
|
emit(const ClassListLoaded(isLoading: true));
|
|
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 {
|
|
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(
|
|
ClassLoadMembers event,
|
|
Emitter<ClassState> emit,
|
|
) async {
|
|
if (state is! ClassDetailLoaded) return;
|
|
final current = state as ClassDetailLoaded;
|
|
emit(current.copyWith(isLoadingMembers: true));
|
|
|
|
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(
|
|
ClassLoadDiaryWall event,
|
|
Emitter<ClassState> emit,
|
|
) async {
|
|
if (state is! ClassDetailLoaded) return;
|
|
final current = state as ClassDetailLoaded;
|
|
emit(current.copyWith(isLoadingWall: true));
|
|
|
|
try {
|
|
// 加载属于该班级的公开日记
|
|
final journals = await _journalRepo.getJournals();
|
|
final classJournals = journals
|
|
.where((j) => j.classId == event.classId && j.sharedToClass)
|
|
.toList();
|
|
|
|
emit(current.copyWith(diaryWall: classJournals, isLoadingWall: false));
|
|
} catch (_) {
|
|
emit(current.copyWith(isLoadingWall: false));
|
|
}
|
|
}
|
|
|
|
Future<void> _onLoadTopics(
|
|
ClassLoadTopics event,
|
|
Emitter<ClassState> emit,
|
|
) async {
|
|
if (state is! ClassDetailLoaded) return;
|
|
final current = state as ClassDetailLoaded;
|
|
|
|
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> _onLoadComments(
|
|
ClassLoadComments event,
|
|
Emitter<ClassState> emit,
|
|
) async {
|
|
if (state is! ClassDetailLoaded) return;
|
|
final current = state as ClassDetailLoaded;
|
|
|
|
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'));
|
|
}
|
|
}
|
|
}
|