// 班级 BLoC — 通过 ClassRepository 管理班级数据 import 'package:flutter/foundation.dart'; 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}); } final class CommentCreate extends ClassEvent { final String journalId; final String content; const CommentCreate({required this.journalId, required this.content}); } // ===== 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 classes; final bool isLoading; final String? error; const ClassListLoaded({this.classes = const [], this.isLoading = false, this.error}); ClassListLoaded copyWith({List? classes, bool? isLoading, String? error, bool clearError = false}) => ClassListLoaded( classes: classes ?? this.classes, isLoading: isLoading ?? this.isLoading, error: clearError ? null : (error ?? this.error), ); } final class ClassDetailLoaded extends ClassState { final SchoolClass classInfo; final List members; final List diaryWall; final List topics; final List comments; final bool isLoadingWall; final bool isLoadingMembers; final String? selectedJournalId; final String? error; 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, this.error, }); ClassDetailLoaded copyWith({ SchoolClass? classInfo, List? members, List? diaryWall, List? topics, List? comments, bool? isLoadingWall, bool? isLoadingMembers, String? selectedJournalId, bool clearSelectedJournal = false, String? error, bool clearError = 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), error: clearError ? null : (error ?? this.error), ); } final class ClassError extends ClassState { final String message; const ClassError(this.message); } // ===== BLoC ===== class ClassBloc extends Bloc { final ClassRepository _classRepo; final JournalRepository _journalRepo; ClassBloc({ required ClassRepository classRepository, required JournalRepository journalRepository, }) : _classRepo = classRepository, _journalRepo = journalRepository, super(const ClassInitial()) { on(_onLoadMyClasses); on(_onClassSelected); on(_onLoadMembers); on(_onLoadDiaryWall); on(_onLoadTopics); on(_onLoadComments); on(_onCreateClass); on(_onTopicAssign); on(_onJoinClass); on(_onCommentCreate); } Future _onLoadMyClasses( ClassLoadMyClasses event, Emitter emit, ) async { emit(const ClassListLoaded(isLoading: true)); try { final classes = await _classRepo.getMyClasses(); emit(ClassListLoaded(classes: classes)); } catch (e) { debugPrint('ClassBloc._onLoadMyClasses 失败: $e'); emit(const ClassListLoaded()); } } Future _onClassSelected( ClassSelected event, Emitter 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) { debugPrint('ClassBloc._onClassSelected 失败: $e'); emit(const ClassError('加载班级失败,请重试')); } } Future _onLoadMembers( ClassLoadMembers event, Emitter 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 (e) { debugPrint('ClassBloc._onLoadMembers 失败: $e'); emit(current.copyWith(isLoadingMembers: false)); } } Future _onLoadDiaryWall( ClassLoadDiaryWall event, Emitter emit, ) async { if (state is! ClassDetailLoaded) return; final current = state as ClassDetailLoaded; emit(current.copyWith(isLoadingWall: true)); try { // 服务端过滤:按 classId 查询班级公开日记(后端 API 已支持 ?class_id= 参数) final journals = await _journalRepo.getJournals(classId: event.classId); final classJournals = journals.where((j) => j.sharedToClass).toList(); emit(current.copyWith(diaryWall: classJournals, isLoadingWall: false)); } catch (e) { debugPrint('ClassBloc._onLoadDiaryWall 失败: $e'); emit(current.copyWith(isLoadingWall: false)); } } Future _onLoadTopics( ClassLoadTopics event, Emitter 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 (e) { debugPrint('ClassBloc._onLoadTopics 失败: $e'); // 保留空列表 } } Future _onLoadComments( ClassLoadComments event, Emitter 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 (e) { debugPrint('ClassBloc._onLoadComments 失败: $e'); emit(current.copyWith(selectedJournalId: event.journalId)); } } Future _onCreateClass( ClassCreate event, Emitter 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) { debugPrint('ClassBloc._onCreateClass 失败: $e'); // 创建失败不改变状态,但通知 UI if (state is ClassListLoaded) { emit((state as ClassListLoaded).copyWith(error: '创建班级失败,请重试')); } } } Future _onTopicAssign( TopicAssign event, Emitter emit, ) async { try { final dto = await _classRepo.assignTopic( classId: event.classId, title: event.title, description: event.description, dueDate: event.dueDate, ); // 更新本地 topics 列表(仅在班级详情视图中) if (state is ClassDetailLoaded) { final current = state as ClassDetailLoaded; 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 (e) { debugPrint('ClassBloc._onTopicAssign 失败: $e'); // 通知 UI 布置失败 if (state is ClassDetailLoaded) { emit((state as ClassDetailLoaded).copyWith(error: '话题布置失败,请重试')); } } } Future _onJoinClass( ClassJoin event, Emitter emit, ) async { try { await _classRepo.joinClass(event.classCode, nickname: event.nickname); // 加入成功后刷新列表 add(const ClassLoadMyClasses()); } catch (e) { debugPrint('ClassBloc._onJoinClass 失败: $e'); emit(const ClassError('加入班级失败,请检查班级码')); } } Future _onCommentCreate( CommentCreate event, Emitter emit, ) async { final currentState = state; if (currentState is! ClassDetailLoaded) return; try { await _classRepo.createComment( journalId: event.journalId, content: event.content, ); // 创建成功后重新加载评语列表 add(ClassLoadComments(event.journalId)); } catch (e) { debugPrint('ClassBloc._onCommentCreate 失败: $e'); emit(currentState.copyWith(error: '评语发布失败')); } } }