// 班级主页 — 日记墙 + 班级信息 + 成员 + 主题 import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:nuanji_app/core/theme/app_colors.dart'; import 'package:nuanji_app/core/theme/app_radius.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'; import '../../auth/bloc/auth_bloc.dart'; import '../bloc/class_bloc.dart'; import '../widgets/comment_bottom_sheet.dart'; /// 班级主页 — 日记墙 + 班级信息 class ClassPage extends StatelessWidget { const ClassPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => ClassBloc( classRepository: context.read(), journalRepository: context.read(), )..add(const ClassLoadMyClasses()), child: const _ClassView(), ); } } class _ClassView extends StatelessWidget { const _ClassView(); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is ClassLoading || state is ClassInitial) { return Scaffold( appBar: AppBar(title: const Text('班级')), body: Center(child: CircularProgressIndicator()), ); } if (state is ClassError) { return Scaffold( appBar: AppBar(title: const Text('班级')), body: Center(child: Text(state.message)), ); } if (state is ClassListLoaded) { return _ClassListView(classes: state.classes, colorScheme: Theme.of(context).colorScheme); } if (state is ClassDetailLoaded) { return _ClassDetailView(state: state); } return const SizedBox.shrink(); }, ); } } // ===== 班级列表视图 ===== class _ClassListView extends StatelessWidget { const _ClassListView({required this.classes, required this.colorScheme}); final List classes; final ColorScheme colorScheme; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('我的班级')), body: classes.isEmpty ? _buildEmptyState(context, colorScheme) : ListView( padding: const EdgeInsets.all(16), children: classes.map((cls) { return _ClassListCard( cls: cls, colorScheme: colorScheme, onTap: () => context.read().add(ClassSelected(cls.id)), ); }).toList(), ), ); } Widget _buildEmptyState(BuildContext context, ColorScheme colorScheme) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.groups_outlined, size: 64, color: colorScheme.onSurface.withValues(alpha: 0.2)), const SizedBox(height: 16), Text('还没有加入任何班级', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), )), const SizedBox(height: 24), FilledButton.tonal( onPressed: () => context.go('/class-code'), child: const Text('输入班级码加入'), ), ], ), ); } } class _ClassListCard extends StatelessWidget { const _ClassListCard({ required this.cls, required this.colorScheme, required this.onTap, }); final SchoolClass cls; final ColorScheme colorScheme; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 0, shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: InkWell( onTap: onTap, borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: AppColors.secondary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), alignment: Alignment.center, child: const Icon(Icons.school_rounded, color: AppColors.secondary), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(cls.name, style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text( '${cls.schoolName} · ${cls.memberCount} 人', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), ), const Icon(Icons.chevron_right), ], ), ), ), ); } } // ===== 班级详情视图(日记墙)===== class _ClassDetailView extends StatelessWidget { const _ClassDetailView({required this.state}); final ClassDetailLoaded state; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final classInfo = state.classInfo; return DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( leading: IconButton( onPressed: () => context.read().add(const ClassLoadMyClasses()), icon: const Icon(Icons.arrow_back), ), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(classInfo.name), Text( '${classInfo.schoolName} · 班级码: ${classInfo.classCode}', style: theme.textTheme.labelSmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), bottom: const TabBar( tabs: [ Tab(text: '日记墙'), Tab(text: '主题'), Tab(text: '成员'), ], ), ), body: TabBarView( children: [ // 日记墙 _DiaryWallTab(state: state), // 主题布置 _TopicsTab(topics: state.topics), // 成员列表 _MembersTab(state: state), ], ), ), ); } } // ===== 日记墙 Tab ===== class _DiaryWallTab extends StatelessWidget { const _DiaryWallTab({required this.state}); final ClassDetailLoaded state; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; if (state.isLoadingWall) { return const Center(child: CircularProgressIndicator()); } if (state.diaryWall.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.auto_stories_outlined, size: 48, color: colorScheme.onSurface.withValues(alpha: 0.2)), const SizedBox(height: 12), Text('日记墙还是空的', style: theme.textTheme.bodyLarge?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), )), const SizedBox(height: 8), Text('分享你的日记到班级吧!', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.3), )), ], ), ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: state.diaryWall.length, itemBuilder: (context, index) { final journal = state.diaryWall[index]; return _DiaryWallCard(journal: journal, comments: state.comments); }, ); } } class _DiaryWallCard extends StatelessWidget { const _DiaryWallCard({required this.journal, required this.comments}); final JournalEntry journal; final List comments; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final moodColor = AppColors.moodColors[journal.mood.value] ?? colorScheme.primary; return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 0, shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: InkWell( onTap: () => context.push('/editor?id=${journal.id}'), borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头部:作者 + 心情 Row( children: [ CircleAvatar( radius: 16, backgroundColor: AppColors.rose.withValues(alpha: 0.2), child: Text( '同', style: theme.textTheme.labelMedium?.copyWith(color: AppColors.rose), ), ), const SizedBox(width: 8), Expanded( child: Text( journal.title, style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), ), Container( width: 28, height: 28, decoration: BoxDecoration( shape: BoxShape.circle, color: moodColor.withValues(alpha: 0.15), ), alignment: Alignment.center, child: Text(_moodEmoji(journal.mood), style: const TextStyle(fontSize: 14)), ), ], ), const SizedBox(height: 8), // 日期 Text( '${journal.date.month}月${journal.date.day}日', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.4), ), ), // 评语 if (comments.isNotEmpty) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.primaryContainer.withValues(alpha: 0.3), borderRadius: AppRadius.smBorder, ), child: Row( children: [ const Icon(Icons.rate_review_rounded, size: 14), const SizedBox(width: 4), Expanded( child: Text( comments.first.content, style: theme.textTheme.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ], // 写评语按钮(仅老师可见) if (_isTeacher(context)) ...[ const SizedBox(height: 8), TextButton.icon( onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => CommentBottomSheet( journalId: journal.id, studentName: journal.authorId, // TODO: 替换为真实昵称 onSubmit: (content) { context.read().add( CommentCreate( journalId: journal.id, content: content, ), ); }, ), ); }, icon: const Icon(Icons.rate_review_outlined, size: 16), label: const Text('写评语'), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 36), ), ), ], ], ), ), ), ); } /// 判断当前用户是否是老师 bool _isTeacher(BuildContext context) { final authState = context.read().state; if (authState is Authenticated) { return authState.user.isTeacher; } return false; } String _moodEmoji(Mood mood) => switch (mood) { Mood.happy => '😊', Mood.calm => '😌', Mood.sad => '😢', Mood.angry => '😠', Mood.thinking => '🤔', }; } // ===== 主题 Tab ===== class _TopicsTab extends StatelessWidget { const _TopicsTab({required this.topics}); final List topics; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; if (topics.isEmpty) { return Center( child: Text('暂无主题布置', style: theme.textTheme.bodyLarge?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), )), ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: topics.length, itemBuilder: (context, index) { final topic = topics[index]; final isOverdue = topic.dueDate != null && topic.dueDate!.isBefore(DateTime.now()); return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 0, shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: InkWell( onTap: () => context.push('/editor?topic=${topic.id}'), borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.assignment_outlined, size: 20), const SizedBox(width: 8), Expanded( child: Text( topic.title, style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), ), if (topic.isActive && !isOverdue) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppColors.secondary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: Text('进行中', style: theme.textTheme.labelSmall?.copyWith(color: AppColors.secondary)), ), ], ), if (topic.description != null) ...[ const SizedBox(height: 8), Text(topic.description!, style: theme.textTheme.bodyMedium), ], if (topic.dueDate != null) ...[ const SizedBox(height: 8), Text( '截止: ${topic.dueDate!.month}月${topic.dueDate!.day}日', style: theme.textTheme.bodySmall?.copyWith( color: isOverdue ? colorScheme.error : colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ], ), ), ), ); }, ); } } // ===== 成员 Tab ===== class _MembersTab extends StatelessWidget { const _MembersTab({required this.state}); final ClassDetailLoaded state; @override Widget build(BuildContext context) { if (state.isLoadingMembers) { return const Center(child: CircularProgressIndicator()); } final theme = Theme.of(context); return ListView.builder( padding: const EdgeInsets.all(16), itemCount: state.members.length, itemBuilder: (context, index) { final member = state.members[index]; final isTeacher = member.role == 'teacher'; return ListTile( leading: CircleAvatar( backgroundColor: isTeacher ? AppColors.accent.withValues(alpha: 0.15) : AppColors.secondary.withValues(alpha: 0.15), child: Text( (member.nickname ?? '?').characters.first, style: TextStyle( color: isTeacher ? AppColors.accent : AppColors.secondary, fontWeight: FontWeight.bold, ), ), ), title: Text(member.nickname ?? '未知'), trailing: isTeacher ? Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppColors.accent.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text('老师', style: theme.textTheme.labelSmall?.copyWith(color: AppColors.accent)), ) : null, ); }, ); } }