Files
wushu/lib/views/profile/components/entire_page.dart
2026-04-01 04:45:33 +08:00

1025 lines
28 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 时间: 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<EntirePage> createState() => _EntirePageState();
}
class _EntirePageState extends State<EntirePage>
with NetworkListenerMixin, SingleTickerProviderStateMixin {
Map<String, dynamic>? _statsData;
String? _errorMessage;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_animationController.forward();
_loadStatsData();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _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<String, dynamic>;
});
sendRefreshEvent();
} else {
setState(() {
_errorMessage = '加载失败:${response.message}';
});
}
} catch (e) {
if (!mounted) return;
setState(() {
_errorMessage = '网络错误:$e';
});
} finally {
endNetworkLoading('stats');
}
}
Future<void> _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<String, dynamic>?);
} 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<SystemUiOverlayStyle>(
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<String, dynamic>;
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<Widget> 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,
),
),
],
),
);
}
}