Files
MookNote/lib/pages/book/book_reviews_page.dart
DelLevin-Home fab952a625 界面优化
2026-07-04 19:44:23 +08:00

326 lines
9.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../../providers/app_provider.dart';
import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import 'book_review_form_page.dart';
import 'book_review_detail_page.dart';
/// 书籍书评列表页面
class BookReviewsPage extends StatefulWidget {
final Book book;
const BookReviewsPage({super.key, required this.book});
@override
State<BookReviewsPage> createState() => _BookReviewsPageState();
}
class _BookReviewsPageState extends State<BookReviewsPage> {
List<BookReview> _reviews = [];
List<BookReview> _filteredReviews = [];
bool _isLoading = true;
bool _isSearching = false;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadReviews();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadReviews() async {
setState(() => _isLoading = true);
final reviews = await context.read<AppProvider>().getBookReviews(widget.book.id);
setState(() {
_reviews = reviews;
_filteredReviews = reviews;
_isLoading = false;
});
}
void _toggleSearch() {
setState(() {
_isSearching = !_isSearching;
if (!_isSearching) {
_searchController.clear();
_filteredReviews = _reviews;
}
});
}
void _onSearchChanged(String query) {
setState(() {
if (query.isEmpty) {
_filteredReviews = _reviews;
} else {
final lowerQuery = query.toLowerCase();
_filteredReviews = _reviews.where((review) {
return review.content.toLowerCase().contains(lowerQuery) ||
review.reviewer.toLowerCase().contains(lowerQuery) ||
review.source.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colors.surface,
appBar: AppBar(
title: _isSearching
? TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: '搜索书评内容、书评人、来源...',
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
border: InputBorder.none,
),
style: TextStyle(color: colors.onSurface),
onChanged: _onSearchChanged,
)
: const Text('书评'),
actions: [
// 搜索按钮
IconButton(
icon: Icon(_isSearching ? Icons.close : Icons.search),
onPressed: _toggleSearch,
),
const SizedBox(width: 8),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _navigateToAddReview(),
icon: const Icon(Icons.add, size: 20),
label: const Text('添加书评'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredReviews.isEmpty
? _buildEmptyState()
: _buildReviewList(),
);
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.rate_review_outlined,
size: 40,
color: colors.onSurface.withValues(alpha: 0.25),
),
),
const SizedBox(height: 20),
Text(
'暂无书评',
style: TextStyle(
fontSize: 16,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 24),
],
),
);
}
Widget _buildReviewList() {
return MasonryGridView.count(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
padding: const EdgeInsets.all(12),
itemCount: _filteredReviews.length,
itemBuilder: (context, index) {
final review = _filteredReviews[index];
return _buildReviewCard(review);
},
);
}
Widget _buildReviewCard(BookReview review) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: () => _navigateToReviewDetail(review),
onLongPress: () => _showDeleteDialog(review),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 类型标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: review.reviewType == 1
? colors.surface
: colors.primary,
borderRadius: BorderRadius.circular(4),
),
child: Text(
review.typeText,
style: TextStyle(
fontSize: 10,
color: review.reviewType == 1
? colors.onSurface.withValues(alpha: 0.6)
: colors.onPrimary,
),
),
),
const SizedBox(height: 8),
// 评论内容
Text(
review.content,
maxLines: review.reviewType == 1 ? 4 : 8,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: colors.onSurface,
height: 1.5,
),
),
const SizedBox(height: 12),
// 底部信息
Row(
children: [
// 书评人
if (review.reviewer.isNotEmpty)
Expanded(
child: Text(
review.reviewer,
style: TextStyle(
fontSize: 11,
color: colors.onSurface.withValues(alpha: 0.6),
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 4),
// 来源和日期一行
Row(
children: [
// 来源
if (review.source.isNotEmpty)
Expanded(
child: Text(
review.source,
style: TextStyle(
fontSize: 10,
color: colors.onSurface.withValues(alpha: 0.4),
),
overflow: TextOverflow.ellipsis,
),
),
// 日期
Text(
_formatDate(review.createdAt),
style: TextStyle(
fontSize: 10,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
],
),
),
);
}
String _formatDate(DateTime date) {
return '${date.year}.${date.month.toString().padLeft(2, '0')}.${date.day.toString().padLeft(2, '0')}';
}
void _navigateToAddReview() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookReviewFormPage(bookId: widget.book.id),
),
).then((_) => _loadReviews());
}
void _navigateToReviewDetail(BookReview review) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookReviewDetailPage(
review: review,
bookId: widget.book.id,
),
),
).then((_) => _loadReviews());
}
void _showDeleteDialog(BookReview review) {
showDialog(
context: context,
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: Text('确定要删除这条书评吗?删除后可在回收站恢复。',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
),
ElevatedButton(
onPressed: () async {
await context.read<AppProvider>().removeBookReview(review.id);
Navigator.pop(context);
_loadReviews();
ToastUtil.show(context, '已删除');
},
style: ElevatedButton.styleFrom(
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('删除'),
),
],
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
);
},
);
}
}