735 lines
21 KiB
Dart
735 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:yx_net_inspector/yx_net_inspector.dart';
|
|
|
|
class SettingsPage extends StatefulWidget {
|
|
const SettingsPage({super.key});
|
|
|
|
@override
|
|
State<SettingsPage> createState() => _SettingsPageState();
|
|
}
|
|
|
|
class _SettingsPageState extends State<SettingsPage> {
|
|
final YxNetInspectorController _controller =
|
|
YxNetInspectorController.instance;
|
|
|
|
// 当前配置状态
|
|
bool _showFloatingBall = true;
|
|
double _ballSize = 70.0;
|
|
Color _ballColor = Colors.blue;
|
|
bool _draggable = true;
|
|
bool _showBadge = true;
|
|
bool _autoHide = false;
|
|
int _maxLogs = 1000;
|
|
|
|
// 主题配置
|
|
Color _primaryColor = Colors.blue;
|
|
Color _successColor = Colors.green;
|
|
Color _errorColor = Colors.red;
|
|
Color _warningColor = Colors.orange;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadCurrentSettings();
|
|
}
|
|
|
|
void _loadCurrentSettings() {
|
|
// 这里可以从持久化存储加载设置
|
|
// 目前使用默认值
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 当前状态卡片
|
|
_buildStatusCard(),
|
|
const SizedBox(height: 20),
|
|
|
|
// 悬浮球设置
|
|
_buildFloatingBallSettings(),
|
|
const SizedBox(height: 20),
|
|
|
|
// 日志设置
|
|
_buildLogSettings(),
|
|
const SizedBox(height: 20),
|
|
|
|
// 主题设置
|
|
_buildThemeSettings(),
|
|
const SizedBox(height: 20),
|
|
|
|
// 操作按钮
|
|
_buildActionButtons(),
|
|
const SizedBox(height: 20),
|
|
|
|
// 统计信息
|
|
_buildStatistics(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusCard() {
|
|
final stats = _controller.getStatistics();
|
|
|
|
return Card(
|
|
elevation: 4,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(
|
|
Icons.dashboard,
|
|
color: Colors.blue,
|
|
size: 24,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'检查器状态',
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatusItem(
|
|
'总请求数',
|
|
stats['totalRequests'].toString(),
|
|
Icons.send,
|
|
Colors.blue,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _buildStatusItem(
|
|
'成功请求',
|
|
stats['successRequests'].toString(),
|
|
Icons.check_circle,
|
|
Colors.green,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatusItem(
|
|
'错误请求',
|
|
stats['errorRequests'].toString(),
|
|
Icons.error,
|
|
Colors.red,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _buildStatusItem(
|
|
'成功率',
|
|
stats['successRate'],
|
|
Icons.trending_up,
|
|
Colors.orange,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusItem(
|
|
String label, String value, IconData icon, Color color) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.05),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: color.withValues(alpha: 0.2)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: color, size: 20),
|
|
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 _buildFloatingBallSettings() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.circle, color: Colors.blue),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'悬浮球设置',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 显示悬浮球开关
|
|
SwitchListTile(
|
|
title: const Text('显示悬浮球'),
|
|
subtitle: const Text('在屏幕上显示网络检查器悬浮球'),
|
|
value: _showFloatingBall,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_showFloatingBall = value;
|
|
});
|
|
if (value) {
|
|
_controller.showFloatingBallWidget();
|
|
} else {
|
|
_controller.hideFloatingBallWidget();
|
|
}
|
|
},
|
|
),
|
|
|
|
// 悬浮球大小
|
|
ListTile(
|
|
title: const Text('悬浮球大小'),
|
|
subtitle: Text('${_ballSize.round()}px'),
|
|
trailing: SizedBox(
|
|
width: 150,
|
|
child: Slider(
|
|
value: _ballSize,
|
|
min: 40,
|
|
max: 100,
|
|
divisions: 12,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_ballSize = value;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
// 悬浮球颜色
|
|
ListTile(
|
|
title: const Text('悬浮球颜色'),
|
|
subtitle: const Text('选择悬浮球的颜色'),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildColorOption(Colors.blue),
|
|
_buildColorOption(Colors.green),
|
|
_buildColorOption(Colors.red),
|
|
_buildColorOption(Colors.purple),
|
|
_buildColorOption(Colors.orange),
|
|
],
|
|
),
|
|
),
|
|
|
|
// 可拖拽开关
|
|
SwitchListTile(
|
|
title: const Text('可拖拽'),
|
|
subtitle: const Text('允许拖拽悬浮球到不同位置'),
|
|
value: _draggable,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_draggable = value;
|
|
});
|
|
},
|
|
),
|
|
|
|
// 显示徽章开关
|
|
SwitchListTile(
|
|
title: const Text('显示数量徽章'),
|
|
subtitle: const Text('在悬浮球上显示请求数量徽章'),
|
|
value: _showBadge,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_showBadge = value;
|
|
});
|
|
},
|
|
),
|
|
|
|
// 自动隐藏开关
|
|
SwitchListTile(
|
|
title: const Text('自动隐藏'),
|
|
subtitle: const Text('一段时间后自动隐藏悬浮球'),
|
|
value: _autoHide,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_autoHide = value;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildColorOption(Color color) {
|
|
final isSelected = _ballColor == color;
|
|
return GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
_ballColor = color;
|
|
});
|
|
},
|
|
child: Container(
|
|
width: 24,
|
|
height: 24,
|
|
margin: const EdgeInsets.only(left: 4),
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: isSelected ? Colors.black : Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: isSelected
|
|
? const Icon(Icons.check, color: Colors.white, size: 14)
|
|
: null,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLogSettings() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.list_alt, color: Colors.green),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'日志设置',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 最大日志数量
|
|
ListTile(
|
|
title: const Text('最大日志数量'),
|
|
subtitle: Text('保留最近 $_maxLogs 条日志记录'),
|
|
trailing: SizedBox(
|
|
width: 150,
|
|
child: Slider(
|
|
value: _maxLogs.toDouble(),
|
|
min: 100,
|
|
max: 5000,
|
|
divisions: 49,
|
|
label: _maxLogs.toString(),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_maxLogs = value.round();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
// 当前日志数量
|
|
ListTile(
|
|
title: const Text('当前日志数量'),
|
|
subtitle: Text('${_controller.logs.length} 条日志'),
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: () {
|
|
setState(() {});
|
|
},
|
|
tooltip: '刷新',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildThemeSettings() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.palette, color: Colors.purple),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'主题设置',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildThemeColorRow('主色调', _primaryColor, (color) {
|
|
setState(() {
|
|
_primaryColor = color;
|
|
});
|
|
}),
|
|
_buildThemeColorRow('成功色', _successColor, (color) {
|
|
setState(() {
|
|
_successColor = color;
|
|
});
|
|
}),
|
|
_buildThemeColorRow('错误色', _errorColor, (color) {
|
|
setState(() {
|
|
_errorColor = color;
|
|
});
|
|
}),
|
|
_buildThemeColorRow('警告色', _warningColor, (color) {
|
|
setState(() {
|
|
_warningColor = color;
|
|
});
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildThemeColorRow(
|
|
String label, Color currentColor, ValueChanged<Color> onColorChanged) {
|
|
final colors = [
|
|
Colors.blue,
|
|
Colors.green,
|
|
Colors.red,
|
|
Colors.orange,
|
|
Colors.purple,
|
|
Colors.teal,
|
|
Colors.pink,
|
|
Colors.indigo,
|
|
];
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
label,
|
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Row(
|
|
children: colors.map((color) {
|
|
final isSelected = currentColor == color;
|
|
return GestureDetector(
|
|
onTap: () => onColorChanged(color),
|
|
child: Container(
|
|
width: 28,
|
|
height: 28,
|
|
margin: const EdgeInsets.only(right: 8),
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: isSelected ? Colors.black : Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: isSelected
|
|
? const Icon(Icons.check, color: Colors.white, size: 16)
|
|
: null,
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButtons() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.settings_applications, color: Colors.orange),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'操作',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _applySettings,
|
|
icon: const Icon(Icons.check),
|
|
label: const Text('应用设置'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _resetSettings,
|
|
icon: const Icon(Icons.restore),
|
|
label: const Text('重置设置'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: _exportLogs,
|
|
icon: const Icon(Icons.download),
|
|
label: const Text('导出日志'),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: _clearAllLogs,
|
|
icon: const Icon(Icons.clear_all),
|
|
label: const Text('清空日志'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatistics() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.analytics, color: Colors.teal),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'详细统计',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildStatItem('GET 请求', _getMethodCount('GET')),
|
|
_buildStatItem('POST 请求', _getMethodCount('POST')),
|
|
_buildStatItem('PUT 请求', _getMethodCount('PUT')),
|
|
_buildStatItem('DELETE 请求', _getMethodCount('DELETE')),
|
|
const Divider(),
|
|
_buildStatItem('平均响应时间', _getAverageResponseTime()),
|
|
_buildStatItem('最近请求时间', _getLastRequestTime()),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatItem(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getMethodCount(String method) {
|
|
return _controller.logs
|
|
.where((log) => log.method == method)
|
|
.length
|
|
.toString();
|
|
}
|
|
|
|
String _getAverageResponseTime() {
|
|
final logsWithDuration =
|
|
_controller.logs.where((log) => log.duration != null);
|
|
if (logsWithDuration.isEmpty) return '0ms';
|
|
|
|
final totalMs = logsWithDuration
|
|
.map((log) => log.duration!.inMilliseconds)
|
|
.reduce((a, b) => a + b);
|
|
|
|
final average = totalMs / logsWithDuration.length;
|
|
return '${average.round()}ms';
|
|
}
|
|
|
|
String _getLastRequestTime() {
|
|
if (_controller.logs.isEmpty) return '无';
|
|
|
|
final lastLog = _controller.logs.first; // logs are sorted by newest first
|
|
final now = DateTime.now();
|
|
final diff = now.difference(lastLog.timestamp);
|
|
|
|
if (diff.inMinutes < 1) {
|
|
return '${diff.inSeconds}秒前';
|
|
} else if (diff.inHours < 1) {
|
|
return '${diff.inMinutes}分钟前';
|
|
} else {
|
|
return '${diff.inHours}小时前';
|
|
}
|
|
}
|
|
|
|
void _applySettings() {
|
|
// 这里可以应用设置到实际的检查器实例
|
|
// 由于演示限制,我们只显示消息
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('设置已应用'),
|
|
backgroundColor: Colors.green,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _resetSettings() {
|
|
setState(() {
|
|
_showFloatingBall = true;
|
|
_ballSize = 70.0;
|
|
_ballColor = Colors.blue;
|
|
_draggable = true;
|
|
_showBadge = true;
|
|
_autoHide = false;
|
|
_maxLogs = 1000;
|
|
_primaryColor = Colors.blue;
|
|
_successColor = Colors.green;
|
|
_errorColor = Colors.red;
|
|
_warningColor = Colors.orange;
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('设置已重置为默认值'),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _exportLogs() {
|
|
if (_controller.logs.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('没有日志可导出'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 这里可以实现实际的导出功能
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('已导出 ${_controller.logs.length} 条日志'),
|
|
backgroundColor: Colors.blue,
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _clearAllLogs() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('确认清空'),
|
|
content: const Text('确定要清空所有日志吗?此操作不可撤销。'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('取消'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
_controller.clearLogs();
|
|
setState(() {});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('所有日志已清空'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
},
|
|
child: const Text('确定'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|