diff --git a/app/lib/features/editor/widgets/comment_list_sheet.dart b/app/lib/features/editor/widgets/comment_list_sheet.dart new file mode 100644 index 0000000..c704c36 --- /dev/null +++ b/app/lib/features/editor/widgets/comment_list_sheet.dart @@ -0,0 +1,262 @@ +// 评论列表底部面板 — FutureBuilder 拉取老师评语 +// +// 独立 widget,不纳入 EditorBloc。 +// 打开日记时从后端拉取评论列表展示。 + +import 'package:flutter/material.dart'; + +import '../../../data/remote/api_client.dart'; +import '../../class_/bloc/class_bloc.dart' show Comment; + +/// 评论列表底部面板 +class CommentListSheet extends StatelessWidget { + final String journalId; + final ApiClient apiClient; + + const CommentListSheet({ + super.key, + required this.journalId, + required this.apiClient, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return DraggableScrollableSheet( + initialChildSize: 0.4, + minChildSize: 0.2, + maxChildSize: 0.7, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(22)), + ), + child: Column( + children: [ + // 拖拽指示条 + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 8), + child: Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + // 标题 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Text( + '老师评语', + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + _CommentCountBadge( + journalId: journalId, + apiClient: apiClient, + ), + ], + ), + ), + const Divider(), + // 评论列表 + Expanded( + child: _CommentListFuture( + journalId: journalId, + apiClient: apiClient, + scrollController: scrollController, + ), + ), + ], + ), + ); + }, + ); + } +} + +/// 评论数量 Badge +class _CommentCountBadge extends StatelessWidget { + final String journalId; + final ApiClient apiClient; + + const _CommentCountBadge({ + required this.journalId, + required this.apiClient, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _fetchComments(), + builder: (context, snapshot) { + if (!snapshot.hasData) return const SizedBox.shrink(); + final count = snapshot.data!.length; + if (count == 0) return const SizedBox.shrink(); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$count', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ); + }, + ); + } + + Future> _fetchComments() async { + try { + final response = await apiClient.get('/diary/journals/$journalId/comments'); + return response.data as List; + } catch (_) { + return []; + } + } +} + +/// 评论列表 — FutureBuilder 拉取 +class _CommentListFuture extends StatelessWidget { + final String journalId; + final ApiClient apiClient; + final ScrollController scrollController; + + const _CommentListFuture({ + required this.journalId, + required this.apiClient, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _fetchComments(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center( + child: Text('加载评语失败', style: TextStyle(color: Colors.grey[500])), + ); + } + final comments = snapshot.data ?? []; + if (comments.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.chat_bubble_outline, size: 36, color: Colors.grey[300]), + const SizedBox(height: 8), + Text( + '还没有评语哦', + style: TextStyle(color: Colors.grey[400]), + ), + ], + ), + ); + } + return ListView.builder( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: comments.length, + itemBuilder: (context, index) { + return _CommentTile(comment: comments[index]); + }, + ); + }, + ); + } + + Future> _fetchComments() async { + try { + final response = await apiClient.get('/diary/journals/$journalId/comments'); + final list = response.data as List; + return list + .map((json) => Comment( + id: json['id'] as String, + journalId: json['journal_id'] as String, + authorId: json['author_id'] as String, + content: json['content'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + )) + .toList(); + } catch (e) { + debugPrint('加载评论失败: $e'); + return []; + } + } +} + +/// 单条评论卡片 +class _CommentTile extends StatelessWidget { + final Comment comment; + + const _CommentTile({required this.comment}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest + .withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 老师标签 + 时间 + Row( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFE07A5F).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '老师', + style: TextStyle(fontSize: 11, color: Color(0xFFE07A5F)), + ), + ), + const SizedBox(width: 8), + Text( + _formatTime(comment.createdAt), + style: TextStyle(fontSize: 11, color: Colors.grey[500]), + ), + ], + ), + const SizedBox(height: 8), + // 评语内容 + Text( + comment.content, + style: const TextStyle(fontSize: 14, height: 1.5), + ), + ], + ), + ), + ); + } + + String _formatTime(DateTime dt) { + return '${dt.month}/${dt.day} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + } +}