feat(app): 添加评论列表展示组件 — FutureBuilder 轮询模式

This commit is contained in:
iven
2026-06-02 23:26:54 +08:00
parent 9e53ca8555
commit c9a69d0be1

View File

@@ -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<List<dynamic>>(
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<List<dynamic>> _fetchComments() async {
try {
final response = await apiClient.get('/diary/journals/$journalId/comments');
return response.data as List<dynamic>;
} 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<List<Comment>>(
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<List<Comment>> _fetchComments() async {
try {
final response = await apiClient.get('/diary/journals/$journalId/comments');
final list = response.data as List<dynamic>;
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')}';
}
}