/// 时间: 2026-04-01 /// 功能: 全站统计页面 /// 介绍: 展示网站统计数据,包括收录数量、热度统计、热门内容等 /// 最新变化: 支持动态主题色设置 library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import '../../../models/colors/app_colors.dart'; import '../../../utils/http/http_client.dart'; import '../../../services/network_listener_service.dart'; import '../../../services/get/theme_controller.dart'; import 'server_info_dialog.dart'; class EntirePage extends StatefulWidget { const EntirePage({super.key}); @override State createState() => _EntirePageState(); } class _EntirePageState extends State with NetworkListenerMixin, SingleTickerProviderStateMixin { final ThemeController _themeController = Get.find(); Map? _statsData; String? _errorMessage; late AnimationController _animationController; late Animation _fadeAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); _animationController.forward(); _loadStatsData(); } @override void dispose() { _animationController.dispose(); super.dispose(); } Future _loadStatsData() async { if (!mounted) return; setState(() { _errorMessage = null; }); startNetworkLoading('stats'); try { final response = await HttpClient.get('app/stats.php'); if (!mounted) return; if (response.isSuccess && response.jsonData['ok'] == true) { setState(() { _statsData = response.jsonData['data'] as Map; }); sendRefreshEvent(); } else { setState(() { _errorMessage = '加载失败:${response.message}'; }); } } catch (e) { if (!mounted) return; setState(() { _errorMessage = '网络错误:$e'; }); } finally { endNetworkLoading('stats'); } } Future _showServerInfo() async { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( backgroundColor: AppColors.surface, content: Row( children: [ CircularProgressIndicator(color: AppColors.primary), const SizedBox(width: 16), Text( '正在检测网络状态...', style: TextStyle(color: AppColors.primaryText), ), ], ), ); }, ); try { final response = await HttpClient.get('poe/load.php'); if (!mounted) return; Navigator.of(context).pop(); if (response.isSuccess) { final data = response.jsonData; if (data['status'] == 'success') { ServerInfoDialog.show(context, data: data as Map?); } else { ServerInfoDialog.show(context); } } else { ServerInfoDialog.show(context); } } catch (e) { if (!mounted) return; Navigator.of(context).pop(); ServerInfoDialog.show(context); } } @override Widget build(BuildContext context) { return Obx(() { final isDark = _themeController.isDarkMode; return AnnotatedRegion( value: isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, child: Scaffold( backgroundColor: AppColors.background, appBar: _buildAppBar(isDark), body: _buildBody(isDark), ), ); }); } PreferredSizeWidget _buildAppBar(bool isDark) { return AppBar( backgroundColor: AppColors.surface, elevation: 0, leading: IconButton( icon: Icon( Icons.arrow_back_ios, color: AppColors.primary, ), onPressed: () => Navigator.pop(context), ), title: Text( '全站统计', style: TextStyle( color: AppColors.primaryText, fontSize: 17, fontWeight: FontWeight.w600, ), ), centerTitle: true, actions: [ IconButton( icon: Icon( Icons.info_outline, color: AppColors.primary, ), onPressed: _showServerInfo, tooltip: '服务器信息', ), ], bottom: PreferredSize( preferredSize: const Size.fromHeight(0.5), child: Container( height: 0.5, color: AppColors.divider, ), ), ); } Widget _buildBody(bool isDark) { if (_errorMessage != null) { return _buildErrorView(isDark); } if (_statsData == null) { return _buildSkeletonView(isDark); } return _buildStatsContent(isDark); } Widget _buildSkeletonBox({ double width = double.infinity, double height = 16, double radius = 8, bool isDark = false, }) { return Container( width: width, height: height, decoration: BoxDecoration( color: isDark ? Colors.grey[700] : AppColors.iosLightGray, borderRadius: BorderRadius.circular(radius), ), ); } Widget _buildSkeletonView(bool isDark) { return FadeTransition( opacity: _fadeAnimation, child: ListView( padding: const EdgeInsets.all(16), children: [ _buildSkeletonHeaderCard(isDark), const SizedBox(height: 16), _buildSkeletonSection(isDark), const SizedBox(height: 16), _buildSkeletonSection(isDark), const SizedBox(height: 16), _buildSkeletonSection(isDark), const SizedBox(height: 16), _buildSkeletonBuildTimeCard(isDark), const SizedBox(height: 32), ], ), ); } Widget _buildSkeletonHeaderCard(bool isDark) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? Colors.grey[700] : AppColors.iosLightGray, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _buildSkeletonBox( width: 28, height: 28, radius: 14, isDark: isDark, ), const SizedBox(width: 12), _buildSkeletonBox(width: 100, height: 22, isDark: isDark), ], ), const SizedBox(height: 12), _buildSkeletonBox(width: 200, height: 14, isDark: isDark), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildSkeletonBox(width: 60, height: 40, isDark: isDark), _buildSkeletonBox(width: 60, height: 40, isDark: isDark), _buildSkeletonBox(width: 60, height: 40, isDark: isDark), ], ), ], ), ); } Widget _buildSkeletonSection(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _buildSkeletonBox( width: 20, height: 20, radius: 10, isDark: isDark, ), const SizedBox(width: 8), _buildSkeletonBox(width: 80, height: 16, isDark: isDark), ], ), const SizedBox(height: 16), GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 3, childAspectRatio: 1.0, crossAxisSpacing: 12, mainAxisSpacing: 12, children: List.generate( 9, (index) => _buildSkeletonCountItem(isDark), ), ), ], ), ); } Widget _buildSkeletonCountItem(bool isDark) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: isDark ? AppColors.darkCard : AppColors.iosLightGray, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Expanded( flex: 2, child: Row( children: [ Expanded( child: _buildSkeletonBox( width: double.infinity, height: 28, radius: 8, isDark: isDark, ), ), const SizedBox(width: 8), Expanded( child: _buildSkeletonBox( width: double.infinity, height: 22, isDark: isDark, ), ), ], ), ), const SizedBox(height: 4), Expanded( flex: 1, child: _buildSkeletonBox(width: 50, height: 12, isDark: isDark), ), ], ), ); } Widget _buildSkeletonBuildTimeCard(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ _buildSkeletonBox(width: 40, height: 40, radius: 10, isDark: isDark), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSkeletonBox(width: 60, height: 14, isDark: isDark), const SizedBox(height: 4), _buildSkeletonBox(width: 150, height: 16, isDark: isDark), ], ), ), ], ), ); } Widget _buildErrorView(bool isDark) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: AppColors.error.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(32), ), child: Icon( Icons.error_outline, color: AppColors.error, size: 32, ), ), const SizedBox(height: 16), Text( _errorMessage ?? '加载失败', style: TextStyle( color: AppColors.tertiaryText, fontSize: 14, ), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: _loadStatsData, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: const Text('重试'), ), ], ), ), ); } Widget _buildStatsContent(bool isDark) { return FadeTransition( opacity: _fadeAnimation, child: RefreshIndicator( color: AppColors.primary, onRefresh: _loadStatsData, child: ListView( padding: const EdgeInsets.all(16), children: [ _buildHeaderCard(), const SizedBox(height: 16), _buildHotSection(isDark), const SizedBox(height: 16), _buildCountSection(isDark), const SizedBox(height: 16), _buildTopContentSection(isDark), const SizedBox(height: 16), _buildBuildTimeCard(isDark), const SizedBox(height: 32), ], ), ), ); } Widget _buildHeaderCard() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary, AppColors.primary.withValues(alpha: 0.8), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppColors.primary.withValues(alpha: 0.3), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.analytics, color: Colors.white, size: 28), const SizedBox(width: 12), const Text( '情景诗词', style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, ), ), const Spacer(), IconButton( icon: const Icon(Icons.refresh, color: Colors.white, size: 22), onPressed: _loadStatsData, tooltip: '刷新数据', ), ], ), const SizedBox(height: 12), Text( '诗意生活,触手可及', style: TextStyle( color: Colors.white.withValues(alpha: 0.9), fontSize: 14, ), ), const SizedBox(height: 16), Row( children: [ _buildHeaderStat( '收录诗句', _statsData?['count_site']?.toString() ?? '0', ), Container( width: 1, height: 40, color: Colors.white.withValues(alpha: 0.3), margin: const EdgeInsets.symmetric(horizontal: 20), ), _buildHeaderStat( '累计热度', _statsData?['cumulative_hits']?.toString() ?? '0', ), Container( width: 1, height: 40, color: Colors.white.withValues(alpha: 0.3), margin: const EdgeInsets.symmetric(horizontal: 20), ), _buildHeaderStat( '累计点赞', _statsData?['cumulative_likes']?.toString() ?? '0', ), ], ), ], ), ); } Widget _buildHeaderStat(String label, String value) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( value, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( label, style: TextStyle( color: Colors.white.withValues(alpha: 0.8), fontSize: 12, ), ), ], ), ); } Widget _buildCountSection(bool isDark) { return _buildSection('数量统计', Icons.format_list_numbered, [ _buildCountGrid(isDark), ], isDark); } Widget _buildCountGrid(bool isDark) { final counts = [ { 'label': '项目', 'value': _statsData?['count_category'] ?? 0, 'icon': Icons.category, 'color': AppColors.iosBlue, 'showIcon': true, }, { 'label': '收录诗句', 'value': _statsData?['count_site'] ?? 0, 'icon': Icons.article, 'color': AppColors.iosGreen, 'showIcon': false, }, { 'label': '审核中', 'value': _statsData?['count_apply'] ?? 0, 'icon': Icons.pending, 'color': AppColors.iosOrange, 'showIcon': true, }, { 'label': '已拒审', 'value': _statsData?['count_apply_reject'] ?? 0, 'icon': Icons.block, 'color': AppColors.iosRed, 'showIcon': true, }, { 'label': '每日一句', 'value': _statsData?['count_article'] ?? 0, 'icon': Icons.wb_sunny, 'color': AppColors.iosPurple, 'showIcon': true, }, { 'label': '文章分类', 'value': _statsData?['count_article_category'] ?? 0, 'icon': Icons.folder, 'color': AppColors.iosPink, 'showIcon': true, }, { 'label': '推送', 'value': _statsData?['count_notice'] ?? 0, 'icon': Icons.campaign, 'color': AppColors.iosTeal, 'showIcon': true, }, { 'label': '开发者', 'value': _statsData?['count_link'] ?? 0, 'icon': Icons.people, 'color': AppColors.iosRed, 'showIcon': true, }, { 'label': '分类标签', 'value': _statsData?['count_tags'] ?? 0, 'icon': Icons.label, 'color': const Color(0xFF64D2FF), 'showIcon': false, }, ]; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1.0, crossAxisSpacing: 12, mainAxisSpacing: 12, ), itemCount: counts.length, itemBuilder: (context, index) { final item = counts[index]; return _buildCountItem( item['label'] as String, item['value'].toString(), item['icon'] as IconData, item['color'] as Color, item['showIcon'] as bool, isDark, ); }, ); } Widget _buildCountItem( String label, String value, IconData icon, Color color, bool showIcon, bool isDark, ) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( children: [ Expanded( flex: 2, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (showIcon) ...[ Expanded( child: Container( decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 24), ), ), const SizedBox(width: 8), ], Expanded( child: Text( value, style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: AppColors.primaryText, ), textAlign: TextAlign.center, ), ), ], ), ), const SizedBox(height: 4), Expanded( flex: 1, child: Center( child: Text( label, style: TextStyle( fontSize: 12, color: AppColors.secondaryText, ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ), ), ], ), ); } Widget _buildHotSection(bool isDark) { return _buildSection('热度统计', Icons.trending_up, [ _buildHotItem( '累计热度', _statsData?['cumulative_hits']?.toString() ?? '0', Icons.local_fire_department, AppColors.iosOrange, isDark, ), const SizedBox(height: 12), _buildHotItem( '累计点赞', _statsData?['cumulative_likes']?.toString() ?? '0', Icons.favorite, AppColors.iosRed, isDark, ), ], isDark); } Widget _buildHotItem( String label, String value, IconData icon, Color color, bool isDark, ) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: color, size: 24), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 14, color: AppColors.secondaryText, ), ), const SizedBox(height: 4), Text( value, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppColors.primaryText, ), ), ], ), ), ], ), ); } Widget _buildTopContentSection(bool isDark) { return _buildSection('热门内容', Icons.star, [ _buildTopContentItem( '今日热门', _statsData?['top_hits_day'], Icons.today, AppColors.iosOrange, isDark, ), const SizedBox(height: 12), _buildTopContentItem( '本月热门', _statsData?['top_hits_month'], Icons.calendar_month, AppColors.iosBlue, isDark, ), const SizedBox(height: 12), _buildTopContentItem( '历史最热', _statsData?['top_hits_total'], Icons.history, AppColors.iosPurple, isDark, ), const SizedBox(height: 12), _buildTopContentItem( '最高点赞', _statsData?['top_like'], Icons.thumb_up, AppColors.iosGreen, isDark, ), ], isDark); } Widget _buildTopContentItem( String label, dynamic data, IconData icon, Color color, bool isDark, ) { final hasData = data != null && data is Map; final content = hasData ? data['name']?.toString() ?? '暂无数据' : '暂无数据'; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: color, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryText, ), ), const SizedBox(height: 8), Text( content, style: TextStyle( fontSize: 13, color: hasData ? AppColors.secondaryText : AppColors.tertiaryText, height: 1.5, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } Widget _buildBuildTimeCard(bool isDark) { final buildTime = _statsData?['build_time']?.toString() ?? '未知'; int days = 0; try { final buildDate = DateTime.parse(buildTime); final now = DateTime.now(); days = now.difference(buildDate).inDays; if (days < 0) days = 0; } catch (e) { days = 0; } return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon( Icons.cake, color: AppColors.primary, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '建站时间', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryText, ), ), const SizedBox(height: 4), Row( children: [ Text( buildTime, style: TextStyle( fontSize: 16, color: AppColors.secondaryText, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Text( '已运行 $days 天', style: TextStyle( fontSize: 12, color: AppColors.primary, fontWeight: FontWeight.w500, ), ), ), ], ), ], ), ), ], ), ); } Widget _buildSection( String title, IconData icon, List children, bool isDark, ) { return Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(icon, color: AppColors.primary, size: 20), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primaryText, ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ), ], ), ); } }