/// 时间: 2026-04-01 /// 功能: 全站统计页面 /// 介绍: 展示网站统计数据,包括收录数量、热度统计、热门内容等 /// 最新变化: 新建页面,iOS风格设计 library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/http_client.dart'; import '../../../services/network_listener_service.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 { 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 const AlertDialog( content: Row( children: [ CircularProgressIndicator(), SizedBox(width: 16), Text('正在检测网络状态...'), ], ), ); }, ); 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 AnnotatedRegion( value: SystemUiOverlayStyle.dark, child: Scaffold( backgroundColor: const Color(0xFFF2F2F7), appBar: _buildAppBar(), body: _buildBody(), ), ); } PreferredSizeWidget _buildAppBar() { return AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton( icon: const Icon( Icons.arrow_back_ios, color: AppConstants.primaryColor, ), onPressed: () => Navigator.pop(context), ), title: const Text( '全站统计', style: TextStyle( color: Colors.black, fontSize: 17, fontWeight: FontWeight.w600, ), ), centerTitle: true, actions: [ IconButton( icon: const Icon( Icons.info_outline, color: AppConstants.primaryColor, ), onPressed: _showServerInfo, tooltip: '服务器信息', ), ], bottom: PreferredSize( preferredSize: const Size.fromHeight(0.5), child: Container(height: 0.5, color: const Color(0xFFE5E5EA)), ), ); } Widget _buildBody() { if (_errorMessage != null) { return _buildErrorView(); } if (_statsData == null) { return _buildSkeletonView(); } return _buildStatsContent(); } Widget _buildSkeletonBox({ double width = double.infinity, double height = 16, double radius = 8, }) { return Container( width: width, height: height, decoration: BoxDecoration( color: const Color(0xFFE5E5EA), borderRadius: BorderRadius.circular(radius), ), ); } Widget _buildSkeletonView() { return FadeTransition( opacity: _fadeAnimation, child: ListView( padding: const EdgeInsets.all(16), children: [ _buildSkeletonHeaderCard(), const SizedBox(height: 16), _buildSkeletonSection(), const SizedBox(height: 16), _buildSkeletonSection(), const SizedBox(height: 16), _buildSkeletonSection(), const SizedBox(height: 16), _buildSkeletonBuildTimeCard(), const SizedBox(height: 32), ], ), ); } Widget _buildSkeletonHeaderCard() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFFE5E5EA), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _buildSkeletonBox(width: 28, height: 28, radius: 14), const SizedBox(width: 12), _buildSkeletonBox(width: 100, height: 22), ], ), const SizedBox(height: 12), _buildSkeletonBox(width: 200, height: 14), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildSkeletonBox(width: 60, height: 40), _buildSkeletonBox(width: 60, height: 40), _buildSkeletonBox(width: 60, height: 40), ], ), ], ), ); } Widget _buildSkeletonSection() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _buildSkeletonBox(width: 20, height: 20, radius: 10), const SizedBox(width: 8), _buildSkeletonBox(width: 80, height: 16), ], ), 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()), ), ], ), ); } Widget _buildSkeletonCountItem() { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: const Color(0xFFF2F2F7), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Expanded( flex: 2, child: Row( children: [ Expanded( child: _buildSkeletonBox( width: double.infinity, height: 28, radius: 8, ), ), const SizedBox(width: 8), Expanded( child: _buildSkeletonBox(width: double.infinity, height: 22), ), ], ), ), const SizedBox(height: 4), Expanded(flex: 1, child: _buildSkeletonBox(width: 50, height: 12)), ], ), ); } Widget _buildSkeletonBuildTimeCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ _buildSkeletonBox(width: 40, height: 40, radius: 10), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSkeletonBox(width: 60, height: 14), const SizedBox(height: 4), _buildSkeletonBox(width: 150, height: 16), ], ), ), ], ), ); } Widget _buildErrorView() { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: const Color(0xFFFF3B30).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(32), ), child: const Icon( Icons.error_outline, color: Color(0xFFFF3B30), size: 32, ), ), const SizedBox(height: 16), Text( _errorMessage ?? '加载失败', style: const TextStyle(color: Color(0xFF8E8E93), fontSize: 14), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: _loadStatsData, style: ElevatedButton.styleFrom( backgroundColor: AppConstants.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: const Text('重试'), ), ], ), ), ); } Widget _buildStatsContent() { return FadeTransition( opacity: _fadeAnimation, child: RefreshIndicator( color: AppConstants.primaryColor, onRefresh: _loadStatsData, child: ListView( padding: const EdgeInsets.all(16), children: [ _buildHeaderCard(), const SizedBox(height: 16), _buildHotSection(), const SizedBox(height: 16), _buildCountSection(), const SizedBox(height: 16), _buildTopContentSection(), const SizedBox(height: 16), _buildBuildTimeCard(), const SizedBox(height: 32), ], ), ), ); } Widget _buildHeaderCard() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppConstants.primaryColor, AppConstants.primaryColor.withValues(alpha: 0.8), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppConstants.primaryColor.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() { return _buildSection('数量统计', Icons.format_list_numbered, [ _buildCountGrid(), ]); } Widget _buildCountGrid() { final counts = [ { 'label': '项目', 'value': _statsData?['count_category'] ?? 0, 'icon': Icons.category, 'color': const Color(0xFF007AFF), 'showIcon': true, }, { 'label': '收录诗句', 'value': _statsData?['count_site'] ?? 0, 'icon': Icons.article, 'color': const Color(0xFF34C759), 'showIcon': false, }, { 'label': '审核中', 'value': _statsData?['count_apply'] ?? 0, 'icon': Icons.pending, 'color': const Color(0xFFFF9500), 'showIcon': true, }, { 'label': '已拒审', 'value': _statsData?['count_apply_reject'] ?? 0, 'icon': Icons.block, 'color': const Color(0xFFFF3B30), 'showIcon': true, }, { 'label': '每日一句', 'value': _statsData?['count_article'] ?? 0, 'icon': Icons.wb_sunny, 'color': const Color(0xFF5856D6), 'showIcon': true, }, { 'label': '文章分类', 'value': _statsData?['count_article_category'] ?? 0, 'icon': Icons.folder, 'color': const Color(0xFFAF52DE), 'showIcon': true, }, { 'label': '推送', 'value': _statsData?['count_notice'] ?? 0, 'icon': Icons.campaign, 'color': const Color(0xFF32ADE6), 'showIcon': true, }, { 'label': '开发者', 'value': _statsData?['count_link'] ?? 0, 'icon': Icons.people, 'color': const Color(0xFFFF2D55), '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, ); }, ); } Widget _buildCountItem( String label, String value, IconData icon, Color color, bool showIcon, ) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( children: [ // 上行:icon和数据,比例2:1(有icon时1:1,无icon时数据占满) 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: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black, ), textAlign: TextAlign.center, ), ), ], ), ), const SizedBox(height: 4), // 下行:描述 Expanded( flex: 1, child: Center( child: Text( label, style: const TextStyle(fontSize: 12, color: Color(0xFF3C3C43)), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ), ), ], ), ); } Widget _buildHotSection() { return _buildSection('热度统计', Icons.trending_up, [ _buildHotItem( '累计热度', _statsData?['cumulative_hits']?.toString() ?? '0', Icons.local_fire_department, const Color(0xFFFF9500), ), const SizedBox(height: 12), _buildHotItem( '累计点赞', _statsData?['cumulative_likes']?.toString() ?? '0', Icons.favorite, const Color(0xFFFF2D55), ), ]); } Widget _buildHotItem(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 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: const TextStyle( fontSize: 14, color: Color(0xFF8E8E93), ), ), const SizedBox(height: 4), Text( value, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black, ), ), ], ), ), ], ), ); } Widget _buildTopContentSection() { return _buildSection('热门内容', Icons.star, [ _buildTopContentItem( '今日热门', _statsData?['top_hits_day'], Icons.today, const Color(0xFFFF9500), ), const SizedBox(height: 12), _buildTopContentItem( '本月热门', _statsData?['top_hits_month'], Icons.calendar_month, const Color(0xFF007AFF), ), const SizedBox(height: 12), _buildTopContentItem( '历史最热', _statsData?['top_hits_total'], Icons.history, const Color(0xFF5856D6), ), const SizedBox(height: 12), _buildTopContentItem( '最高点赞', _statsData?['top_like'], Icons.thumb_up, const Color(0xFF34C759), ), ]); } Widget _buildTopContentItem( String label, dynamic data, IconData icon, Color color, ) { final hasData = data != null && data is Map; final content = hasData ? data['name']?.toString() ?? '暂无数据' : '暂无数据'; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 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: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black, ), ), const SizedBox(height: 8), Text( content, style: TextStyle( fontSize: 13, color: hasData ? const Color(0xFF3C3C43) : const Color(0xFF8E8E93), height: 1.5, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } Widget _buildBuildTimeCard() { 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: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppConstants.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.cake, color: AppConstants.primaryColor, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '建站时间', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black, ), ), const SizedBox(height: 4), Row( children: [ Text( buildTime, style: const TextStyle( fontSize: 16, color: Color(0xFF3C3C43), ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: AppConstants.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Text( '已运行 $days 天', style: const TextStyle( fontSize: 12, color: AppConstants.primaryColor, fontWeight: FontWeight.w500, ), ), ), ], ), ], ), ), ], ), ); } Widget _buildSection(String title, IconData icon, List children) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 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: AppConstants.primaryColor, size: 20), const SizedBox(width: 8), Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black, ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ), ], ), ); } }