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