feat: server conn statistics (#888)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-09-02 19:41:56 +08:00
committed by GitHub
parent 929061213f
commit 2466341999
39 changed files with 2534 additions and 19 deletions

View File

@@ -0,0 +1,360 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/data/res/store.dart';
class ConnectionStatsPage extends StatefulWidget {
const ConnectionStatsPage({super.key});
@override
State<ConnectionStatsPage> createState() => _ConnectionStatsPageState();
}
class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
List<ServerConnectionStats> _serverStats = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadStats();
}
void _loadStats() {
setState(() {
_isLoading = true;
});
final stats = Stores.connectionStats.getAllServerStats();
setState(() {
_serverStats = stats;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: Text(l10n.connectionStats),
actions: [
IconButton(
onPressed: _loadStats,
icon: const Icon(Icons.refresh),
tooltip: libL10n.refresh,
),
IconButton(
onPressed: _showClearAllDialog,
icon: const Icon(Icons.clear_all, color: Colors.red),
tooltip: libL10n.clear,
),
],
),
body: _buildBody,
);
}
Widget get _buildBody {
if (_isLoading) {
return const Center(child: SizedLoading.large);
}
if (_serverStats.isEmpty) {
return Center(child: Text(l10n.noConnectionStatsData));
}
return ListView.builder(
itemCount: _serverStats.length,
itemBuilder: (context, index) {
final stats = _serverStats[index];
return _buildServerStatsCard(stats);
},
);
}
Widget _buildServerStatsCard(ServerConnectionStats stats) {
final successRate = stats.totalAttempts == 0
? 'N/A'
: '${(stats.successRate * 100).toStringAsFixed(1)}%';
final lastSuccessTime = stats.lastSuccessTime;
final lastFailureTime = stats.lastFailureTime;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
stats.serverName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
Text(
'${libL10n.success}: $successRate%',
style: TextStyle(
fontSize: 16,
color: stats.successRate >= 0.8
? Colors.green
: stats.successRate >= 0.5
? Colors.orange
: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
l10n.totalAttempts,
stats.totalAttempts.toString(),
Icons.all_inclusive,
),
_buildStatItem(
libL10n.success,
stats.successCount.toString(),
Icons.check_circle,
Colors.green,
),
_buildStatItem(
libL10n.fail,
stats.failureCount.toString(),
Icons.error,
Colors.red,
),
],
),
if (lastSuccessTime != null || lastFailureTime != null) ...[
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
if (lastSuccessTime != null)
_buildTimeItem(
l10n.lastSuccess,
lastSuccessTime,
Icons.check_circle,
Colors.green,
),
if (lastFailureTime != null)
_buildTimeItem(
l10n.lastFailure,
lastFailureTime,
Icons.error,
Colors.red,
),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.recentConnections,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () => _showServerDetailsDialog(stats),
child: Text(l10n.viewDetails),
),
],
),
const SizedBox(height: 8),
...stats.recentConnections.take(3).map(_buildConnectionItem),
],
),
),
);
}
Widget _buildStatItem(
String label,
String value,
IconData icon, [
Color? color,
]) {
return Column(
children: [
Icon(icon, size: 24, color: color ?? Colors.grey),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
);
}
Widget _buildTimeItem(
String label,
DateTime time,
IconData icon,
Color color,
) {
final timeStr = time.simple();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 16, color: color),
UIs.width7,
Text(
'$label: ',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
Text(timeStr, style: const TextStyle(fontSize: 12)),
],
),
);
}
Widget _buildConnectionItem(ConnectionStat stat) {
final timeStr = stat.timestamp.simple();
final isSuccess = stat.result.isSuccess;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.error,
size: 16,
color: isSuccess ? Colors.green : Colors.red,
),
UIs.width7,
Text(timeStr, style: const TextStyle(fontSize: 12)),
UIs.width7,
Expanded(
child: Text(
isSuccess
? '${libL10n.success} (${stat.durationMs}ms)'
: stat.result.displayName,
style: TextStyle(
fontSize: 12,
color: isSuccess ? Colors.green : Colors.red,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
extension on _ConnectionStatsPageState {
void _showServerDetailsDialog(ServerConnectionStats stats) {
context.showRoundDialog(
title: '${stats.serverName} - ${l10n.connectionDetails}',
child: SizedBox(
width: double.maxFinite,
height: MediaQuery.sizeOf(context).height * 0.7,
child: ListView.separated(
itemCount: stats.recentConnections.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
final stat = stats.recentConnections[index];
final timeStr = stat.timestamp.simple();
final isSuccess = stat.result.isSuccess;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: isSuccess ? Colors.green : Colors.red,
),
title: Text(timeStr),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isSuccess
? '${libL10n.success} (${stat.durationMs}ms)'
: '${libL10n.fail}: ${stat.result.displayName}',
style: TextStyle(
color: isSuccess ? Colors.green : Colors.red,
),
),
if (!isSuccess && stat.errorMessage.isNotEmpty)
Text(
stat.errorMessage,
style: const TextStyle(fontSize: 11, color: Colors.grey),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
),
actions: [
TextButton(onPressed: context.pop, child: Text(libL10n.close)),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_showClearServerStatsDialog(stats);
},
child: Text(
l10n.clearThisServerStats,
style: TextStyle(color: Colors.red),
),
),
],
);
}
void _showClearAllDialog() {
context.showRoundDialog(
title: l10n.clearAllStatsTitle,
child: Text(l10n.clearAllStatsContent),
actions: [
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
CountDownBtn(
onTap: () {
context.pop();
Stores.connectionStats.clearAll();
_loadStats();
},
text: libL10n.ok,
afterColor: Colors.red,
),
],
);
}
void _showClearServerStatsDialog(ServerConnectionStats stats) {
context.showRoundDialog(
title: l10n.clearServerStatsTitle(stats.serverName),
child: Text(l10n.clearServerStatsContent(stats.serverName)),
actions: [
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
CountDownBtn(
onTap: () {
context.pop();
Stores.connectionStats.clearServerStats(stats.serverId);
_loadStats();
},
text: libL10n.ok,
afterColor: Colors.red,
),
],
);
}
}

View File

@@ -9,6 +9,7 @@ extension _Server on _AppSettingsPageState {
_buildNetViewType(),
_buildServerSeq(),
_buildServerDetailCardSeq(),
_buildConnectionStats(),
_buildDeleteServers(),
_buildCpuView(),
_buildServerMore(),
@@ -38,6 +39,22 @@ extension _Server on _AppSettingsPageState {
);
}
Widget _buildConnectionStats() {
return ListTile(
leading: const Icon(Icons.analytics, size: _kIconSize),
title: const Text('连接统计'),
subtitle: const Text('查看服务器连接成功率和历史记录'),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ConnectionStatsPage(),
),
);
},
);
}
Widget _buildDeleteServers() {
return ListTile(
title: Text(l10n.deleteServers),

View File

@@ -17,6 +17,7 @@ import 'package:server_box/data/store/setting.dart';
import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/view/page/backup.dart';
import 'package:server_box/view/page/private_key/list.dart';
import 'package:server_box/view/page/server/connection_stats.dart';
import 'package:server_box/view/page/setting/platform/android.dart';
import 'package:server_box/view/page/setting/platform/ios.dart';
import 'package:server_box/view/page/setting/platform/platform_pub.dart';