Files
nj/app/lib/features/class_/bloc/class_bloc.dart
iven 49d4aa36a7
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): Phase 1.1 紧急修复 — SyncEngine 接入 + authorId + catch 异常处理
- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync
- fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local'
- fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint
- feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter)
- feat(editor): EditorBloc 扩展 + EditorPage 增强
- feat(search): SearchBloc 扩展搜索功能
- feat(home): HomeBloc/HomePage 增强
- feat(auth): LoginPage 增强
- feat(templates): TemplateGalleryPage 重构
- fix(web): 管理端班级/日记页面修复
- fix(server): comment_service + theme_handler 修复
- docs: 添加全链路审计报告和验证截图
2026-06-02 21:21:43 +08:00

426 lines
12 KiB
Dart

// 班级 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<SchoolClass> classes;
final bool isLoading;
final String? error;
const ClassListLoaded({this.classes = const [], this.isLoading = false, this.error});
ClassListLoaded copyWith({List<SchoolClass>? 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<ClassMember> members;
final List<JournalEntry> diaryWall;
final List<TopicAssignment> topics;
final List<Comment> 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<ClassMember>? members,
List<JournalEntry>? diaryWall,
List<TopicAssignment>? topics,
List<Comment>? 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<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);
on<CommentCreate>(_onCommentCreate);
}
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) {
debugPrint('ClassBloc._onLoadMyClasses 失败: $e');
emit(const ClassListLoaded());
}
}
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) {
debugPrint('ClassBloc._onClassSelected 失败: $e');
emit(const ClassError('加载班级失败,请重试'));
}
}
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 (e) {
debugPrint('ClassBloc._onLoadMembers 失败: $e');
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 {
// 服务端过滤:按 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<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 (e) {
debugPrint('ClassBloc._onLoadTopics 失败: $e');
// 保留空列表
}
}
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 (e) {
debugPrint('ClassBloc._onLoadComments 失败: $e');
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) {
debugPrint('ClassBloc._onCreateClass 失败: $e');
// 创建失败不改变状态,但通知 UI
if (state is ClassListLoaded) {
emit((state as ClassListLoaded).copyWith(error: '创建班级失败,请重试'));
}
}
}
Future<void> _onTopicAssign(
TopicAssign event,
Emitter<ClassState> 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<void> _onJoinClass(
ClassJoin event,
Emitter<ClassState> emit,
) async {
try {
await _classRepo.joinClass(event.classCode, nickname: event.nickname);
// 加入成功后刷新列表
add(const ClassLoadMyClasses());
} catch (e) {
debugPrint('ClassBloc._onJoinClass 失败: $e');
emit(const ClassError('加入班级失败,请检查班级码'));
}
}
Future<void> _onCommentCreate(
CommentCreate event,
Emitter<ClassState> 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: '评语发布失败'));
}
}
}