From 3eaf83c79a5e423c2260a983b11257cdf225c2e2 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 1 Jun 2026 22:49:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E8=80=81=E5=B8=88=E7=82=B9?= =?UTF-8?q?=E8=AF=84=E5=8A=9F=E8=83=BD=20=E2=80=94=20CommentCreate?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=20+=20CommentBottomSheet=20+=20=E6=97=A5?= =?UTF-8?q?=E8=AE=B0=E5=A2=99=E7=82=B9=E8=AF=84=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/features/class_/bloc/class_bloc.dart | 31 +++ app/lib/features/class_/views/class_page.dart | 56 ++++- .../class_/widgets/comment_bottom_sheet.dart | 191 ++++++++++++++++++ 3 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 app/lib/features/class_/widgets/comment_bottom_sheet.dart diff --git a/app/lib/features/class_/bloc/class_bloc.dart b/app/lib/features/class_/bloc/class_bloc.dart index 021f803..3b1fef6 100644 --- a/app/lib/features/class_/bloc/class_bloc.dart +++ b/app/lib/features/class_/bloc/class_bloc.dart @@ -66,6 +66,12 @@ final class ClassJoin extends ClassEvent { 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 { @@ -125,6 +131,7 @@ final class ClassDetailLoaded extends ClassState { final bool isLoadingWall; final bool isLoadingMembers; final String? selectedJournalId; + final String? error; const ClassDetailLoaded({ required this.classInfo, @@ -135,6 +142,7 @@ final class ClassDetailLoaded extends ClassState { this.isLoadingWall = false, this.isLoadingMembers = false, this.selectedJournalId, + this.error, }); ClassDetailLoaded copyWith({ @@ -147,6 +155,8 @@ final class ClassDetailLoaded extends ClassState { bool? isLoadingMembers, String? selectedJournalId, bool clearSelectedJournal = false, + String? error, + bool clearError = false, }) => ClassDetailLoaded( classInfo: classInfo ?? this.classInfo, @@ -157,6 +167,7 @@ final class ClassDetailLoaded extends ClassState { isLoadingWall: isLoadingWall ?? this.isLoadingWall, isLoadingMembers: isLoadingMembers ?? this.isLoadingMembers, selectedJournalId: clearSelectedJournal ? null : (selectedJournalId ?? this.selectedJournalId), + error: clearError ? null : (error ?? this.error), ); } @@ -186,6 +197,7 @@ class ClassBloc extends Bloc { on(_onCreateClass); on(_onTopicAssign); on(_onJoinClass); + on(_onCommentCreate); } Future _onLoadMyClasses( @@ -369,4 +381,23 @@ class ClassBloc extends Bloc { emit(ClassError('加入班级失败: $e')); } } + + 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) { + emit(currentState.copyWith(error: '评语发布失败')); + } + } } diff --git a/app/lib/features/class_/views/class_page.dart b/app/lib/features/class_/views/class_page.dart index 76aa561..4134cbc 100644 --- a/app/lib/features/class_/views/class_page.dart +++ b/app/lib/features/class_/views/class_page.dart @@ -4,11 +4,14 @@ 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 { @@ -129,12 +132,12 @@ class _ClassListCard extends StatelessWidget { margin: const EdgeInsets.only(bottom: 12), elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(16), + borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -292,12 +295,12 @@ class _DiaryWallCard extends StatelessWidget { margin: const EdgeInsets.only(bottom: 12), elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: InkWell( onTap: () => context.push('/editor?id=${journal.id}'), - borderRadius: BorderRadius.circular(16), + borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -348,7 +351,7 @@ class _DiaryWallCard extends StatelessWidget { padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.primaryContainer.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(8), + borderRadius: AppRadius.xsBorder, ), child: Row( children: [ @@ -366,6 +369,36 @@ class _DiaryWallCard extends StatelessWidget { ), ), ], + // 写评语按钮(仅老师可见) + 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), + ), + ), + ], ], ), ), @@ -373,6 +406,15 @@ class _DiaryWallCard extends StatelessWidget { ); } + /// 判断当前用户是否是老师 + 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 => '😌', @@ -413,12 +455,12 @@ class _TopicsTab extends StatelessWidget { margin: const EdgeInsets.only(bottom: 12), elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: InkWell( onTap: () => context.push('/editor?topic=${topic.id}'), - borderRadius: BorderRadius.circular(16), + borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.all(16), child: Column( diff --git a/app/lib/features/class_/widgets/comment_bottom_sheet.dart b/app/lib/features/class_/widgets/comment_bottom_sheet.dart new file mode 100644 index 0000000..25d61b7 --- /dev/null +++ b/app/lib/features/class_/widgets/comment_bottom_sheet.dart @@ -0,0 +1,191 @@ +// 评语输入 BottomSheet — 老师点评学生日记 +// +// 设计要点: +// - 7 个快捷评语模板,一键选择 +// - 自由文字输入,支持多行 +// - 温暖鼓励的语气(面向小学生日记) +// - 触摸目标 ≥ 44px + +import 'package:flutter/material.dart'; + +/// 老师点评输入面板 +class CommentBottomSheet extends StatefulWidget { + final String journalId; + final String studentName; + final void Function(String content) onSubmit; + + const CommentBottomSheet({ + super.key, + required this.journalId, + required this.studentName, + required this.onSubmit, + }); + + @override + State createState() => _CommentBottomSheetState(); +} + +class _CommentBottomSheetState extends State { + final _controller = TextEditingController(); + final _focusNode = FocusNode(); + bool _isSubmitting = false; + + // 快捷评语模板 — 温暖鼓励风格 + static const _quickComments = [ + '🌟 写得真好!继续加油!', + '📖 故事很精彩,想象力很丰富!', + '💪 字写得很工整,继续保持!', + '🎨 画得很漂亮,很有创意!', + '🌈 内容很丰富,观察很仔细!', + '✍️ 可以再多写一点自己的感受哦', + '📸 照片拍得很好,记录很用心!', + ]; + + @override + void initState() { + super.initState(); + Future.microtask(() => _focusNode.requestFocus()); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _submit() { + final content = _controller.text.trim(); + if (content.isEmpty) return; + + setState(() => _isSubmitting = true); + widget.onSubmit(content); + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(22)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 拖拽条 + Center( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + // 标题 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '点评 ${widget.studentName} 的日记', + style: theme.textTheme.titleSmall, + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => Navigator.pop(context), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + const SizedBox(height: 8), + + // 快捷评语 + SizedBox( + height: 80, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: _quickComments.map((comment) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: ActionChip( + label: Text(comment, style: const TextStyle(fontSize: 12)), + onPressed: () { + _controller.text = comment; + _focusNode.requestFocus(); + }, + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 8), + + // 输入框 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: _controller, + focusNode: _focusNode, + maxLines: 3, + minLines: 1, + textInputAction: TextInputAction.done, + onSubmitted: (_) => _submit(), + decoration: InputDecoration( + hintText: '写下你的评语...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: 2, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + + // 提交按钮 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + width: double.infinity, + height: 44, + child: FilledButton( + onPressed: _isSubmitting ? null : _submit, + child: _isSubmitting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('发布评语'), + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ); + } +}