feat(app): 添加评论列表展示组件 — FutureBuilder 轮询模式
This commit is contained in:
262
app/lib/features/editor/widgets/comment_list_sheet.dart
Normal file
262
app/lib/features/editor/widgets/comment_list_sheet.dart
Normal 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')}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user