import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; import '../utils/user_prefs.dart'; /// 数据统计页面 - 多维度数据分析 class StatisticsPage extends StatefulWidget { const StatisticsPage({super.key}); @override State createState() => _StatisticsPageState(); } class _StatisticsPageState extends State { int _selectedTab = 0; bool get _showMovies => UserPrefs().showMovieTab; bool get _showBooks => UserPrefs().showBookTab; bool get _showNotes => UserPrefs().showNoteTab; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: const Text('数据统计'), ), body: Consumer( builder: (context, provider, child) { final movies = provider.movies; final books = provider.books; final notes = provider.notes; return Column( children: [ _buildTabBar(), Expanded( child: _selectedTab == 0 ? _buildOverviewTab(movies, books, notes) : _buildTrendTab(movies, books, notes), ), ], ); }, ), ); } Widget _buildTabBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Row( children: [ Expanded(child: _buildTabItem('概览', 0)), const SizedBox(width: 12), Expanded(child: _buildTabItem('趋势', 1)), ], ), ); } Widget _buildTabItem(String label, int index) { final isSelected = _selectedTab == index; return GestureDetector( onTap: () => setState(() => _selectedTab = index), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8), ), child: Text( label, textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, color: isSelected ? Colors.white : const Color(0xFF666666), ), ), ), ); } Widget _buildOverviewTab(List movies, List books, List notes) { final totalMovies = movies.where((m) => !m.isDeleted).length; final totalBooks = books.length; final totalNotes = notes.length; final watchedMovies = movies.where((m) => m.status == 'watched' && !m.isDeleted).length; final watchingMovies = movies.where((m) => m.status == 'watching' && !m.isDeleted).length; final wantToWatchMovies = movies.where((m) => m.status == 'want_to_watch' && !m.isDeleted).length; final readBooks = books.where((b) => b.status == 'read').length; final readingBooks = books.where((b) => b.status == 'reading').length; final wantToReadBooks = books.where((b) => b.status == 'want_to_read').length; final movieRatings = movies.where((m) => m.rating != null && !m.isDeleted).map((m) => m.rating!); final avgMovieRating = movieRatings.isNotEmpty ? movieRatings.reduce((a, b) => a + b) / movieRatings.length : null; final bookRatings = books.where((b) => b.rating != null).map((b) => b.rating!); final avgBookRating = bookRatings.isNotEmpty ? bookRatings.reduce((a, b) => a + b) / bookRatings.length : null; final children = []; // 数据总览 children.add(_buildSectionTitle('数据总览')); children.add(const SizedBox(height: 16)); children.add(_buildTotalOverview(totalMovies, totalBooks, totalNotes)); children.add(const SizedBox(height: 32)); // 影视状态分布 if (_showMovies) { children.add(_buildSectionTitle('影视')); children.add(const SizedBox(height: 16)); children.add(_buildStatusDistribution([ _StatusData('已看', watchedMovies, const Color(0xFF1A1A1A)), _StatusData('在看', watchingMovies, const Color(0xFF666666)), _StatusData('想看', wantToWatchMovies, const Color(0xFF999999)), ], avgRating: avgMovieRating)); children.add(const SizedBox(height: 32)); } // 书籍状态分布 if (_showBooks) { children.add(_buildSectionTitle('书籍')); children.add(const SizedBox(height: 16)); children.add(_buildStatusDistribution([ _StatusData('已读', readBooks, const Color(0xFF1A1A1A)), _StatusData('在读', readingBooks, const Color(0xFF666666)), _StatusData('想读', wantToReadBooks, const Color(0xFF999999)), ], avgRating: avgBookRating)); children.add(const SizedBox(height: 32)); } // 最近活动 children.add(_buildSectionTitle('最近7天活动')); children.add(const SizedBox(height: 16)); children.add(_buildRecentActivity(movies, books, notes)); return ListView( padding: const EdgeInsets.all(24), children: children, ); } Widget _buildTotalOverview(int totalMovies, int totalBooks, int totalNotes) { final items = []; if (_showMovies) items.add(Expanded(child: _buildStatItem('影视', totalMovies, Icons.movie_outlined))); if (_showBooks) items.add(Expanded(child: _buildStatItem('书籍', totalBooks, Icons.menu_book_outlined))); if (_showNotes) items.add(Expanded(child: _buildStatItem('笔记', totalNotes, Icons.note_outlined))); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFFF8F8F8), borderRadius: BorderRadius.circular(12), ), child: Row(children: items), ); } Widget _buildTrendTab(List movies, List books, List notes) { final now = DateTime.now(); final dates = List.generate(30, (index) => now.subtract(Duration(days: 29 - index))); final movieTrend = dates.map((date) { return movies.where((m) { final created = m.createdAt; return created.year == date.year && created.month == date.month && created.day == date.day && !m.isDeleted; }).length; }).toList(); final bookTrend = dates.map((date) { return books.where((b) { final created = b.createdAt; return created.year == date.year && created.month == date.month && created.day == date.day; }).length; }).toList(); final noteTrend = dates.map((date) { return notes.where((n) { final created = n.createdAt; return created.year == date.year && created.month == date.month && created.day == date.day; }).length; }).toList(); final totalMovies = movieTrend.fold(0, (a, b) => a + b); final totalBooks = bookTrend.fold(0, (a, b) => a + b); final totalNotes = noteTrend.fold(0, (a, b) => a + b); final children = []; children.add(_buildSectionTitle('近30天新增')); children.add(const SizedBox(height: 16)); children.add(_buildTrendSummaryRow(totalMovies, totalBooks, totalNotes)); children.add(const SizedBox(height: 32)); children.add(_buildSectionTitle('数据趋势')); children.add(const SizedBox(height: 16)); children.add(_buildTrendChart(movieTrend, bookTrend, noteTrend)); return ListView( padding: const EdgeInsets.all(24), children: children, ); } Widget _buildTrendSummaryRow(int totalMovies, int totalBooks, int totalNotes) { final items = []; if (_showMovies) items.add(Expanded(child: _buildTrendSummary('影视', totalMovies))); if (_showBooks) items.add(Expanded(child: _buildTrendSummary('书籍', totalBooks))); if (_showNotes) items.add(Expanded(child: _buildTrendSummary('笔记', totalNotes))); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFFF8F8F8), borderRadius: BorderRadius.circular(12), ), child: Row(children: items), ); } Widget _buildTrendChart(List movieTrend, List bookTrend, List noteTrend) { final rows = []; if (_showMovies) { rows.add(_buildTrendRow('影视', movieTrend, const Color(0xFF1A1A1A))); } if (_showBooks) { if (rows.isNotEmpty) rows.add(const SizedBox(height: 16)); rows.add(_buildTrendRow('书籍', bookTrend, const Color(0xFF666666))); } if (_showNotes) { if (rows.isNotEmpty) rows.add(const SizedBox(height: 16)); rows.add(_buildTrendRow('笔记', noteTrend, const Color(0xFF999999))); } return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12), ), child: Column(children: rows), ); } Widget _buildTrendSummary(String label, int count) { return Column( children: [ Text(count.toString(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A))), const SizedBox(height: 4), Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF666666))), ], ); } Widget _buildTrendRow(String label, List data, Color color) { final maxValue = data.isEmpty ? 1 : data.reduce((a, b) => a > b ? a : b); final safeMax = maxValue == 0 ? 1 : maxValue; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: color)), const SizedBox(height: 8), SizedBox( height: 40, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: data.map((value) { final height = value / safeMax * 40; return Expanded( child: Container( margin: const EdgeInsets.symmetric(horizontal: 1), height: height < 2 ? 2 : height, decoration: BoxDecoration( color: color.withOpacity(value == 0 ? 0.1 : 0.6), borderRadius: BorderRadius.circular(2), ), ), ); }).toList(), ), ), ], ); } Widget _buildStatItem(String title, int count, IconData icon) { return Column( children: [ Icon(icon, size: 24, color: const Color(0xFF666666)), const SizedBox(height: 8), Text(count.toString(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A))), const SizedBox(height: 4), Text(title, style: const TextStyle(fontSize: 13, color: Color(0xFF666666))), ], ); } Widget _buildStatusDistribution(List<_StatusData> data, {double? avgRating}) { final total = data.fold(0, (sum, item) => sum + item.count); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ ...data.map((item) { final percentage = total > 0 ? (item.count / total * 100).toStringAsFixed(1) : '0'; return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Container( width: 10, height: 10, decoration: BoxDecoration(color: item.color, borderRadius: BorderRadius.circular(2)), ), const SizedBox(width: 12), Text(item.label, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))), const Spacer(), Text('${item.count}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 8), Text('($percentage%)', style: const TextStyle(fontSize: 13, color: Color(0xFF999999))), ], ), ); }), if (avgRating != null) ...[ const Divider(height: 1, color: Color(0xFFE8E8E8)), const SizedBox(height: 12), Row( children: [ const Icon(Icons.star, size: 16, color: Color(0xFF999999)), const SizedBox(width: 10), const Text('平均评分', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))), const Spacer(), Text(avgRating.toStringAsFixed(1), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ], ), ], ], ), ); } Widget _buildRecentActivity(List movies, List books, List notes) { final now = DateTime.now(); final sevenDaysAgo = now.subtract(const Duration(days: 7)); final recentMovies = movies.where((m) => m.createdAt.isAfter(sevenDaysAgo) && !m.isDeleted).length; final recentBooks = books.where((b) => b.createdAt.isAfter(sevenDaysAgo)).length; final recentNotes = notes.where((n) => n.createdAt.isAfter(sevenDaysAgo)).length; final items = []; if (_showMovies) { items.add(_buildActivityRow('新增影视', recentMovies, Icons.movie_outlined)); } if (_showBooks) { if (items.isNotEmpty) items.add(const Divider(height: 20, color: Color(0xFFE8E8E8))); items.add(_buildActivityRow('新增书籍', recentBooks, Icons.menu_book_outlined)); } if (_showNotes) { if (items.isNotEmpty) items.add(const Divider(height: 20, color: Color(0xFFE8E8E8))); items.add(_buildActivityRow('新增笔记', recentNotes, Icons.note_outlined)); } return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12), ), child: Column(children: items), ); } Widget _buildActivityRow(String label, int count, IconData icon) { return Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), ), child: Icon(icon, size: 18, color: const Color(0xFF666666)), ), const SizedBox(width: 12), Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))), const Spacer(), Text('$count', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 4), const Text('个', style: TextStyle(fontSize: 13, color: Color(0xFF999999))), ], ); } Widget _buildSectionTitle(String title) { return Row( children: [ Container( width: 4, height: 16, decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2)), ), const SizedBox(width: 8), Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ], ); } } class _StatusData { final String label; final int count; final Color color; _StatusData(this.label, this.count, this.color); }