feat: server conn statistics (#888)
This commit is contained in:
360
lib/view/page/server/connection_stats.dart
Normal file
360
lib/view/page/server/connection_stats.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user