feat(app): 初始化家庭网络监控Flutter应用基本结构
- 添加Flutter项目基础文件与配置,包括.gitignore和analysis_options.yaml - 配置Android相关文件及Gradle构建脚本支持 - 新增设备状态与网络状态数据模型及相关枚举 - 实现网络检测及网速测试服务 - 创建监控状态管理Provider,实现设备状态管理和自动刷新机制 - 编写主界面HomeScreen,包括设备列表、网速仪表盘及基础交互 - 添加资源文件支持应用图标和启动画面 - 配置项目元数据文件,支持Flutter迁移及版本控制 - 新增项目README,提供入门指引和相关资源链接
This commit is contained in:
39
home_monitor/lib/main.dart
Normal file
39
home_monitor/lib/main.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'providers/providers.dart';
|
||||
import 'screens/screens.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const HomeMonitorApp());
|
||||
}
|
||||
|
||||
class HomeMonitorApp extends StatelessWidget {
|
||||
const HomeMonitorApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => MonitorProvider(),
|
||||
child: MaterialApp(
|
||||
title: '家庭网络监控',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
themeMode: ThemeMode.system,
|
||||
home: const HomeScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
127
home_monitor/lib/models/device_status.dart
Normal file
127
home_monitor/lib/models/device_status.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
/// 设备状态枚举
|
||||
enum DeviceState {
|
||||
online, // 在线
|
||||
offline, // 离线
|
||||
warning, // 警告
|
||||
unknown, // 未知
|
||||
}
|
||||
|
||||
/// 设备类型枚举
|
||||
enum DeviceType {
|
||||
gateway, // 网关/主路由
|
||||
bypass, // 旁路由
|
||||
nas, // NAS存储
|
||||
server, // 服务器
|
||||
camera, // 摄像头
|
||||
printer, // 打印机
|
||||
other, // 其他设备
|
||||
}
|
||||
|
||||
/// 设备状态模型
|
||||
class DeviceStatus {
|
||||
final String id;
|
||||
final String name;
|
||||
final DeviceType type;
|
||||
final String ip;
|
||||
final int port;
|
||||
final DeviceState state;
|
||||
final int? latency; // 延迟(ms)
|
||||
final DateTime lastCheck; // 最后检测时间
|
||||
final String? errorMessage; // 错误信息
|
||||
|
||||
const DeviceStatus({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.ip,
|
||||
this.port = 80,
|
||||
this.state = DeviceState.unknown,
|
||||
this.latency,
|
||||
required this.lastCheck,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// 从JSON创建
|
||||
factory DeviceStatus.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceStatus(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
type: DeviceType.values.firstWhere(
|
||||
(e) => e.name == json['type'],
|
||||
orElse: () => DeviceType.other,
|
||||
),
|
||||
ip: json['ip'] as String,
|
||||
port: json['port'] as int? ?? 80,
|
||||
state: DeviceState.values.firstWhere(
|
||||
(e) => e.name == json['state'],
|
||||
orElse: () => DeviceState.unknown,
|
||||
),
|
||||
latency: json['latency'] as int?,
|
||||
lastCheck: DateTime.parse(json['lastCheck'] as String),
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type.name,
|
||||
'ip': ip,
|
||||
'port': port,
|
||||
'state': state.name,
|
||||
'latency': latency,
|
||||
'lastCheck': lastCheck.toIso8601String(),
|
||||
'errorMessage': errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
/// 复制并更新
|
||||
DeviceStatus copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
DeviceType? type,
|
||||
String? ip,
|
||||
int? port,
|
||||
DeviceState? state,
|
||||
int? latency,
|
||||
DateTime? lastCheck,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return DeviceStatus(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
ip: ip ?? this.ip,
|
||||
port: port ?? this.port,
|
||||
state: state ?? this.state,
|
||||
latency: latency ?? this.latency,
|
||||
lastCheck: lastCheck ?? this.lastCheck,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
/// 是否在线
|
||||
bool get isOnline => state == DeviceState.online;
|
||||
|
||||
/// 获取设备类型图标名称
|
||||
String get iconName {
|
||||
switch (type) {
|
||||
case DeviceType.gateway:
|
||||
return 'router';
|
||||
case DeviceType.bypass:
|
||||
return 'alt_route';
|
||||
case DeviceType.nas:
|
||||
return 'storage';
|
||||
case DeviceType.server:
|
||||
return 'dns';
|
||||
case DeviceType.camera:
|
||||
return 'videocam';
|
||||
case DeviceType.printer:
|
||||
return 'print';
|
||||
case DeviceType.other:
|
||||
return 'devices';
|
||||
}
|
||||
}
|
||||
}
|
||||
3
home_monitor/lib/models/models.dart
Normal file
3
home_monitor/lib/models/models.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// 模型层导出文件
|
||||
export 'device_status.dart';
|
||||
export 'network_status.dart';
|
||||
109
home_monitor/lib/models/network_status.dart
Normal file
109
home_monitor/lib/models/network_status.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
/// 网络状态模型
|
||||
class NetworkStatus {
|
||||
final bool isConnected; // 是否连接
|
||||
final double downloadSpeed; // 下载速度 (Mbps)
|
||||
final double uploadSpeed; // 上传速度 (Mbps)
|
||||
final int latency; // 延迟 (ms)
|
||||
final String externalIp; // 外网IP
|
||||
final String localIp; // 内网IP
|
||||
final DateTime lastCheck; // 最后检测时间
|
||||
|
||||
const NetworkStatus({
|
||||
this.isConnected = false,
|
||||
this.downloadSpeed = 0,
|
||||
this.uploadSpeed = 0,
|
||||
this.latency = 0,
|
||||
this.externalIp = '',
|
||||
this.localIp = '',
|
||||
required this.lastCheck,
|
||||
});
|
||||
|
||||
/// 默认状态
|
||||
factory NetworkStatus.initial() {
|
||||
return NetworkStatus(lastCheck: DateTime.now());
|
||||
}
|
||||
|
||||
/// 从JSON创建
|
||||
factory NetworkStatus.fromJson(Map<String, dynamic> json) {
|
||||
return NetworkStatus(
|
||||
isConnected: json['isConnected'] as bool? ?? false,
|
||||
downloadSpeed: (json['downloadSpeed'] as num?)?.toDouble() ?? 0,
|
||||
uploadSpeed: (json['uploadSpeed'] as num?)?.toDouble() ?? 0,
|
||||
latency: json['latency'] as int? ?? 0,
|
||||
externalIp: json['externalIp'] as String? ?? '',
|
||||
localIp: json['localIp'] as String? ?? '',
|
||||
lastCheck: DateTime.parse(json['lastCheck'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'isConnected': isConnected,
|
||||
'downloadSpeed': downloadSpeed,
|
||||
'uploadSpeed': uploadSpeed,
|
||||
'latency': latency,
|
||||
'externalIp': externalIp,
|
||||
'localIp': localIp,
|
||||
'lastCheck': lastCheck.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 复制并更新
|
||||
NetworkStatus copyWith({
|
||||
bool? isConnected,
|
||||
double? downloadSpeed,
|
||||
double? uploadSpeed,
|
||||
int? latency,
|
||||
String? externalIp,
|
||||
String? localIp,
|
||||
DateTime? lastCheck,
|
||||
}) {
|
||||
return NetworkStatus(
|
||||
isConnected: isConnected ?? this.isConnected,
|
||||
downloadSpeed: downloadSpeed ?? this.downloadSpeed,
|
||||
uploadSpeed: uploadSpeed ?? this.uploadSpeed,
|
||||
latency: latency ?? this.latency,
|
||||
externalIp: externalIp ?? this.externalIp,
|
||||
localIp: localIp ?? this.localIp,
|
||||
lastCheck: lastCheck ?? this.lastCheck,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取网络状态描述
|
||||
String get statusDescription {
|
||||
if (!isConnected) return '网络断开';
|
||||
if (latency > 200) return '网络较慢';
|
||||
if (latency > 100) return '网络一般';
|
||||
return '网络良好';
|
||||
}
|
||||
|
||||
/// 格式化下载速度
|
||||
String get downloadSpeedText {
|
||||
if (downloadSpeed >= 1000) {
|
||||
return '${(downloadSpeed / 1000).toStringAsFixed(2)} Gbps';
|
||||
}
|
||||
return '${downloadSpeed.toStringAsFixed(2)} Mbps';
|
||||
}
|
||||
|
||||
/// 格式化上传速度
|
||||
String get uploadSpeedText {
|
||||
if (uploadSpeed >= 1000) {
|
||||
return '${(uploadSpeed / 1000).toStringAsFixed(2)} Gbps';
|
||||
}
|
||||
return '${uploadSpeed.toStringAsFixed(2)} Mbps';
|
||||
}
|
||||
}
|
||||
|
||||
/// 网速历史记录
|
||||
class SpeedHistory {
|
||||
final DateTime time;
|
||||
final double download;
|
||||
final double upload;
|
||||
|
||||
const SpeedHistory({
|
||||
required this.time,
|
||||
required this.download,
|
||||
required this.upload,
|
||||
});
|
||||
}
|
||||
169
home_monitor/lib/providers/monitor_provider.dart
Normal file
169
home_monitor/lib/providers/monitor_provider.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/models.dart';
|
||||
import '../services/services.dart';
|
||||
|
||||
/// 监控状态管理
|
||||
class MonitorProvider extends ChangeNotifier {
|
||||
final NetworkService _networkService = NetworkService();
|
||||
final SpeedTestService _speedTestService = SpeedTestService();
|
||||
|
||||
// 状态
|
||||
NetworkStatus _networkStatus = NetworkStatus.initial();
|
||||
List<DeviceStatus> _devices = [];
|
||||
bool _isLoading = false;
|
||||
bool _isTestingSpeed = false;
|
||||
String? _errorMessage;
|
||||
|
||||
// 定时器
|
||||
Timer? _autoRefreshTimer;
|
||||
int _refreshInterval = 30; // 秒
|
||||
|
||||
// Getters
|
||||
NetworkStatus get networkStatus => _networkStatus;
|
||||
List<DeviceStatus> get devices => _devices;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isTestingSpeed => _isTestingSpeed;
|
||||
String? get errorMessage => _errorMessage;
|
||||
int get refreshInterval => _refreshInterval;
|
||||
|
||||
// 在线设备数量
|
||||
int get onlineDevicesCount =>
|
||||
_devices.where((d) => d.isOnline).length;
|
||||
|
||||
// 离线设备数量
|
||||
int get offlineDevicesCount =>
|
||||
_devices.where((d) => d.state == DeviceState.offline).length;
|
||||
|
||||
/// 初始化默认设备列表
|
||||
void initDefaultDevices() {
|
||||
_devices = [
|
||||
DeviceStatus(
|
||||
id: '1',
|
||||
name: '主路由器',
|
||||
type: DeviceType.gateway,
|
||||
ip: '192.168.1.1',
|
||||
port: 80,
|
||||
lastCheck: DateTime.now(),
|
||||
),
|
||||
DeviceStatus(
|
||||
id: '2',
|
||||
name: '旁路由',
|
||||
type: DeviceType.bypass,
|
||||
ip: '192.168.1.2',
|
||||
port: 80,
|
||||
lastCheck: DateTime.now(),
|
||||
),
|
||||
DeviceStatus(
|
||||
id: '3',
|
||||
name: 'NAS',
|
||||
type: DeviceType.nas,
|
||||
ip: '192.168.1.100',
|
||||
port: 5000,
|
||||
lastCheck: DateTime.now(),
|
||||
),
|
||||
];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 添加设备
|
||||
void addDevice(DeviceStatus device) {
|
||||
_devices.add(device);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 移除设备
|
||||
void removeDevice(String id) {
|
||||
_devices.removeWhere((d) => d.id == id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 更新设备
|
||||
void updateDevice(DeviceStatus device) {
|
||||
final index = _devices.indexWhere((d) => d.id == device.id);
|
||||
if (index != -1) {
|
||||
_devices[index] = device;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新所有状态
|
||||
Future<void> refreshAll() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// 检测网络状态
|
||||
_networkStatus = await _speedTestService.quickCheck();
|
||||
|
||||
// 检测所有设备
|
||||
if (_devices.isNotEmpty) {
|
||||
_devices = await _networkService.checkAllDevices(_devices);
|
||||
}
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行网速测试
|
||||
Future<void> runSpeedTest() async {
|
||||
_isTestingSpeed = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_networkStatus = await _speedTestService.runFullTest();
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
_isTestingSpeed = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 检测单个设备
|
||||
Future<void> checkSingleDevice(String deviceId) async {
|
||||
final index = _devices.indexWhere((d) => d.id == deviceId);
|
||||
if (index == -1) return;
|
||||
|
||||
final device = _devices[index];
|
||||
final updatedDevice = await _networkService.checkDevice(device);
|
||||
_devices[index] = updatedDevice;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 启动自动刷新
|
||||
void startAutoRefresh({int intervalSeconds = 30}) {
|
||||
_refreshInterval = intervalSeconds;
|
||||
stopAutoRefresh();
|
||||
|
||||
_autoRefreshTimer = Timer.periodic(
|
||||
Duration(seconds: intervalSeconds),
|
||||
(_) => refreshAll(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 停止自动刷新
|
||||
void stopAutoRefresh() {
|
||||
_autoRefreshTimer?.cancel();
|
||||
_autoRefreshTimer = null;
|
||||
}
|
||||
|
||||
/// 设置刷新间隔
|
||||
void setRefreshInterval(int seconds) {
|
||||
_refreshInterval = seconds;
|
||||
if (_autoRefreshTimer != null) {
|
||||
startAutoRefresh(intervalSeconds: seconds);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stopAutoRefresh();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
2
home_monitor/lib/providers/providers.dart
Normal file
2
home_monitor/lib/providers/providers.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
// Provider导出文件
|
||||
export 'monitor_provider.dart';
|
||||
259
home_monitor/lib/screens/home_screen.dart
Normal file
259
home_monitor/lib/screens/home_screen.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/providers.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
/// 主页面
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始化数据
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = context.read<MonitorProvider>();
|
||||
provider.initDefaultDevices();
|
||||
provider.refreshAll();
|
||||
provider.startAutoRefresh(intervalSeconds: 30);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
title: const Text('家庭网络监控'),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
Consumer<MonitorProvider>(
|
||||
builder: (context, provider, _) {
|
||||
return IconButton(
|
||||
icon: provider.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: provider.isLoading ? null : () => provider.refreshAll(),
|
||||
tooltip: '刷新',
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
_showSettingsDialog();
|
||||
},
|
||||
tooltip: '设置',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Consumer<MonitorProvider>(
|
||||
builder: (context, provider, _) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => provider.refreshAll(),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 网速仪表盘
|
||||
SpeedGauge(
|
||||
networkStatus: provider.networkStatus,
|
||||
isLoading: provider.isTestingSpeed,
|
||||
),
|
||||
|
||||
// 概览卡片
|
||||
_buildOverviewCards(provider),
|
||||
|
||||
// 设备列表标题
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'监控设备',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => _showAddDeviceDialog(),
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: const Text('添加'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 设备列表
|
||||
_buildDeviceList(provider),
|
||||
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewCards(MonitorProvider provider) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatusCard(
|
||||
title: '在线设备',
|
||||
value: '${provider.onlineDevicesCount}',
|
||||
icon: Icons.check_circle,
|
||||
iconColor: Colors.green,
|
||||
subtitle: '共 ${provider.devices.length} 台设备',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: StatusCard(
|
||||
title: '离线设备',
|
||||
value: '${provider.offlineDevicesCount}',
|
||||
icon: Icons.error,
|
||||
iconColor: provider.offlineDevicesCount > 0
|
||||
? Colors.red
|
||||
: Colors.grey,
|
||||
subtitle: provider.offlineDevicesCount > 0
|
||||
? '需要检查'
|
||||
: '全部正常',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceList(MonitorProvider provider) {
|
||||
if (provider.devices.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.devices, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('暂无监控设备'),
|
||||
SizedBox(height: 8),
|
||||
Text('点击右上角添加设备', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: provider.devices.length,
|
||||
itemBuilder: (context, index) {
|
||||
final device = provider.devices[index];
|
||||
return DeviceTile(
|
||||
device: device,
|
||||
onRefresh: () => provider.checkSingleDevice(device.id),
|
||||
onTap: () => _showDeviceDetail(device),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
return Consumer<MonitorProvider>(
|
||||
builder: (context, provider, _) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: provider.isTestingSpeed ? null : () => provider.runSpeedTest(),
|
||||
icon: provider.isTestingSpeed
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.speed),
|
||||
label: Text(provider.isTestingSpeed ? '测速中...' : '测速'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSettingsDialog() {
|
||||
final provider = context.read<MonitorProvider>();
|
||||
int interval = provider.refreshInterval;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('设置'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('自动刷新间隔'),
|
||||
subtitle: Text('$interval 秒'),
|
||||
trailing: DropdownButton<int>(
|
||||
value: interval,
|
||||
items: const [
|
||||
DropdownMenuItem(value: 10, child: Text('10秒')),
|
||||
DropdownMenuItem(value: 30, child: Text('30秒')),
|
||||
DropdownMenuItem(value: 60, child: Text('1分钟')),
|
||||
DropdownMenuItem(value: 300, child: Text('5分钟')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
provider.setRefreshInterval(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddDeviceDialog() {
|
||||
// TODO: 实现添加设备对话框
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('添加设备功能开发中...')),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeviceDetail(device) {
|
||||
// TODO: 实现设备详情页
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('设备: ${device.name}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
home_monitor/lib/screens/screens.dart
Normal file
2
home_monitor/lib/screens/screens.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
// Screens导出文件
|
||||
export 'home_screen.dart';
|
||||
119
home_monitor/lib/services/network_service.dart
Normal file
119
home_monitor/lib/services/network_service.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import '../models/models.dart';
|
||||
|
||||
/// 网络检测服务
|
||||
class NetworkService {
|
||||
static final NetworkService _instance = NetworkService._internal();
|
||||
factory NetworkService() => _instance;
|
||||
NetworkService._internal();
|
||||
|
||||
/// Ping指定主机
|
||||
Future<int?> ping(String host, {int timeout = 5000}) async {
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
// 尝试建立TCP连接来检测
|
||||
final socket = await Socket.connect(
|
||||
host,
|
||||
80,
|
||||
timeout: Duration(milliseconds: timeout),
|
||||
);
|
||||
|
||||
stopwatch.stop();
|
||||
socket.destroy();
|
||||
|
||||
return stopwatch.elapsedMilliseconds;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 检测设备是否在线
|
||||
Future<DeviceStatus> checkDevice(DeviceStatus device) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final socket = await Socket.connect(
|
||||
device.ip,
|
||||
device.port,
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
stopwatch.stop();
|
||||
socket.destroy();
|
||||
|
||||
return device.copyWith(
|
||||
state: DeviceState.online,
|
||||
latency: stopwatch.elapsedMilliseconds,
|
||||
lastCheck: DateTime.now(),
|
||||
errorMessage: null,
|
||||
);
|
||||
} on SocketException catch (e) {
|
||||
return device.copyWith(
|
||||
state: DeviceState.offline,
|
||||
latency: null,
|
||||
lastCheck: DateTime.now(),
|
||||
errorMessage: e.message,
|
||||
);
|
||||
} catch (e) {
|
||||
return device.copyWith(
|
||||
state: DeviceState.unknown,
|
||||
latency: null,
|
||||
lastCheck: DateTime.now(),
|
||||
errorMessage: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量检测设备
|
||||
Future<List<DeviceStatus>> checkAllDevices(List<DeviceStatus> devices) async {
|
||||
final results = await Future.wait(
|
||||
devices.map((device) => checkDevice(device)),
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
/// 检测网络连通性(通过多个公共DNS)
|
||||
Future<bool> checkInternetConnection() async {
|
||||
final hosts = [
|
||||
'8.8.8.8', // Google DNS
|
||||
'114.114.114.114', // 114 DNS
|
||||
'223.5.5.5', // 阿里 DNS
|
||||
];
|
||||
|
||||
for (final host in hosts) {
|
||||
final latency = await ping(host);
|
||||
if (latency != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 获取本地IP地址
|
||||
Future<String> getLocalIp() async {
|
||||
try {
|
||||
final interfaces = await NetworkInterface.list(
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
|
||||
for (final interface in interfaces) {
|
||||
for (final addr in interface.addresses) {
|
||||
if (!addr.isLoopback) {
|
||||
return addr.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
return '未知';
|
||||
}
|
||||
|
||||
/// 测量到指定主机的延迟
|
||||
Future<int> measureLatency(String host) async {
|
||||
final latency = await ping(host);
|
||||
return latency ?? -1;
|
||||
}
|
||||
}
|
||||
3
home_monitor/lib/services/services.dart
Normal file
3
home_monitor/lib/services/services.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// 服务层导出文件
|
||||
export 'network_service.dart';
|
||||
export 'speed_test_service.dart';
|
||||
128
home_monitor/lib/services/speed_test_service.dart
Normal file
128
home_monitor/lib/services/speed_test_service.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../models/models.dart';
|
||||
import 'network_service.dart';
|
||||
|
||||
/// 网速测试服务
|
||||
class SpeedTestService {
|
||||
static final SpeedTestService _instance = SpeedTestService._internal();
|
||||
factory SpeedTestService() => _instance;
|
||||
SpeedTestService._internal();
|
||||
|
||||
final Dio _dio = Dio();
|
||||
|
||||
// 测试文件URL(可配置)
|
||||
static const _testUrls = [
|
||||
'https://speed.cloudflare.com/__down?bytes=10000000', // 10MB
|
||||
'https://speed.hetzner.de/100MB.bin',
|
||||
];
|
||||
|
||||
/// 测试下载速度
|
||||
Future<double> testDownloadSpeed({int testDurationSeconds = 5}) async {
|
||||
double totalBytes = 0;
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final response = await _dio.get<List<int>>(
|
||||
_testUrls[0],
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
receiveTimeout: Duration(seconds: testDurationSeconds + 2),
|
||||
),
|
||||
onReceiveProgress: (received, total) {
|
||||
totalBytes = received.toDouble();
|
||||
},
|
||||
);
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
// 计算速度 (Mbps)
|
||||
final seconds = stopwatch.elapsedMilliseconds / 1000;
|
||||
final mbps = (totalBytes * 8) / (seconds * 1000000);
|
||||
|
||||
return mbps;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试上传速度(简化版,通过POST测试)
|
||||
Future<double> testUploadSpeed({int testDurationSeconds = 5}) async {
|
||||
// 生成测试数据 (1MB)
|
||||
final testData = List.generate(1024 * 1024, (i) => i % 256);
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
await _dio.post(
|
||||
'https://speed.cloudflare.com/__up',
|
||||
data: testData,
|
||||
options: Options(
|
||||
sendTimeout: Duration(seconds: testDurationSeconds + 2),
|
||||
),
|
||||
);
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
final seconds = stopwatch.elapsedMilliseconds / 1000;
|
||||
final mbps = (testData.length * 8) / (seconds * 1000000);
|
||||
|
||||
return mbps;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 完整网速测试
|
||||
Future<NetworkStatus> runFullTest() async {
|
||||
final networkService = NetworkService();
|
||||
|
||||
// 检测连接
|
||||
final isConnected = await networkService.checkInternetConnection();
|
||||
if (!isConnected) {
|
||||
return NetworkStatus(
|
||||
isConnected: false,
|
||||
lastCheck: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// 获取本地IP
|
||||
final localIp = await networkService.getLocalIp();
|
||||
|
||||
// 测量延迟
|
||||
final latency = await networkService.measureLatency('8.8.8.8');
|
||||
|
||||
// 测试下载速度
|
||||
final downloadSpeed = await testDownloadSpeed();
|
||||
|
||||
// 测试上传速度
|
||||
final uploadSpeed = await testUploadSpeed();
|
||||
|
||||
return NetworkStatus(
|
||||
isConnected: true,
|
||||
downloadSpeed: downloadSpeed,
|
||||
uploadSpeed: uploadSpeed,
|
||||
latency: latency,
|
||||
localIp: localIp,
|
||||
lastCheck: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 快速网络检测(仅检测连通性和延迟)
|
||||
Future<NetworkStatus> quickCheck() async {
|
||||
final networkService = NetworkService();
|
||||
|
||||
final isConnected = await networkService.checkInternetConnection();
|
||||
final localIp = await networkService.getLocalIp();
|
||||
final latency = isConnected
|
||||
? await networkService.measureLatency('8.8.8.8')
|
||||
: -1;
|
||||
|
||||
return NetworkStatus(
|
||||
isConnected: isConnected,
|
||||
latency: latency,
|
||||
localIp: localIp,
|
||||
lastCheck: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
154
home_monitor/lib/widgets/device_tile.dart
Normal file
154
home_monitor/lib/widgets/device_tile.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
/// 设备列表项组件
|
||||
class DeviceTile extends StatelessWidget {
|
||||
final DeviceStatus device;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onRefresh;
|
||||
|
||||
const DeviceTile({
|
||||
super.key,
|
||||
required this.device,
|
||||
this.onTap,
|
||||
this.onRefresh,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final statusColor = _getStatusColor();
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
leading: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
_getDeviceIcon(),
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
device.name,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${device.ip}:${device.port}',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
_StatusBadge(
|
||||
status: _getStatusText(),
|
||||
color: statusColor,
|
||||
),
|
||||
if (device.latency != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${device.latency}ms',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: onRefresh,
|
||||
tooltip: '刷新',
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getDeviceIcon() {
|
||||
switch (device.type) {
|
||||
case DeviceType.gateway:
|
||||
return Icons.router;
|
||||
case DeviceType.bypass:
|
||||
return Icons.alt_route;
|
||||
case DeviceType.nas:
|
||||
return Icons.storage;
|
||||
case DeviceType.server:
|
||||
return Icons.dns;
|
||||
case DeviceType.camera:
|
||||
return Icons.videocam;
|
||||
case DeviceType.printer:
|
||||
return Icons.print;
|
||||
case DeviceType.other:
|
||||
return Icons.devices;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusColor() {
|
||||
switch (device.state) {
|
||||
case DeviceState.online:
|
||||
return Colors.green;
|
||||
case DeviceState.offline:
|
||||
return Colors.red;
|
||||
case DeviceState.warning:
|
||||
return Colors.orange;
|
||||
case DeviceState.unknown:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusText() {
|
||||
switch (device.state) {
|
||||
case DeviceState.online:
|
||||
return '在线';
|
||||
case DeviceState.offline:
|
||||
return '离线';
|
||||
case DeviceState.warning:
|
||||
return '警告';
|
||||
case DeviceState.unknown:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态徽章
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
final String status;
|
||||
final Color color;
|
||||
|
||||
const _StatusBadge({
|
||||
required this.status,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
199
home_monitor/lib/widgets/speed_gauge.dart
Normal file
199
home_monitor/lib/widgets/speed_gauge.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
/// 网速仪表盘组件
|
||||
class SpeedGauge extends StatelessWidget {
|
||||
final NetworkStatus networkStatus;
|
||||
final bool isLoading;
|
||||
|
||||
const SpeedGauge({
|
||||
super.key,
|
||||
required this.networkStatus,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'网络状态',
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (isLoading)
|
||||
const SizedBox(
|
||||
height: 120,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
_buildSpeedDisplay(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildConnectionInfo(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeedDisplay(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// 下载速度
|
||||
_SpeedIndicator(
|
||||
label: '下载',
|
||||
value: networkStatus.downloadSpeedText,
|
||||
icon: Icons.download,
|
||||
color: Colors.blue,
|
||||
),
|
||||
// 延迟
|
||||
_SpeedIndicator(
|
||||
label: '延迟',
|
||||
value: networkStatus.latency >= 0
|
||||
? '${networkStatus.latency}ms'
|
||||
: '--',
|
||||
icon: Icons.speed,
|
||||
color: _getLatencyColor(networkStatus.latency),
|
||||
),
|
||||
// 上传速度
|
||||
_SpeedIndicator(
|
||||
label: '上传',
|
||||
value: networkStatus.uploadSpeedText,
|
||||
icon: Icons.upload,
|
||||
color: Colors.green,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectionInfo(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_InfoItem(
|
||||
label: '状态',
|
||||
value: networkStatus.isConnected ? '已连接' : '未连接',
|
||||
valueColor: networkStatus.isConnected ? Colors.green : Colors.red,
|
||||
),
|
||||
_InfoItem(
|
||||
label: '本地IP',
|
||||
value: networkStatus.localIp.isEmpty
|
||||
? '--'
|
||||
: networkStatus.localIp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getLatencyColor(int latency) {
|
||||
if (latency < 0) return Colors.grey;
|
||||
if (latency > 200) return Colors.red;
|
||||
if (latency > 100) return Colors.orange;
|
||||
return Colors.green;
|
||||
}
|
||||
}
|
||||
|
||||
/// 速度指示器
|
||||
class _SpeedIndicator extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const _SpeedIndicator({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: color, size: 28),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 信息项
|
||||
class _InfoItem extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Color? valueColor;
|
||||
|
||||
const _InfoItem({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.valueColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: valueColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
143
home_monitor/lib/widgets/status_card.dart
Normal file
143
home_monitor/lib/widgets/status_card.dart
Normal file
@@ -0,0 +1,143 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 状态卡片组件
|
||||
class StatusCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color? iconColor;
|
||||
final Color? backgroundColor;
|
||||
final String? subtitle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const StatusCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
this.iconColor,
|
||||
this.backgroundColor,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
color: backgroundColor ?? theme.cardColor,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: iconColor ?? theme.colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 网络状态指示器
|
||||
class NetworkStatusIndicator extends StatelessWidget {
|
||||
final bool isConnected;
|
||||
final int latency;
|
||||
|
||||
const NetworkStatusIndicator({
|
||||
super.key,
|
||||
required this.isConnected,
|
||||
required this.latency,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = _getStatusColor();
|
||||
final text = _getStatusText();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor() {
|
||||
if (!isConnected) return Colors.red;
|
||||
if (latency < 0) return Colors.grey;
|
||||
if (latency > 200) return Colors.orange;
|
||||
if (latency > 100) return Colors.yellow.shade700;
|
||||
return Colors.green;
|
||||
}
|
||||
|
||||
String _getStatusText() {
|
||||
if (!isConnected) return '离线';
|
||||
if (latency < 0) return '检测中';
|
||||
return '${latency}ms';
|
||||
}
|
||||
}
|
||||
4
home_monitor/lib/widgets/widgets.dart
Normal file
4
home_monitor/lib/widgets/widgets.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
// Widgets导出文件
|
||||
export 'status_card.dart';
|
||||
export 'device_tile.dart';
|
||||
export 'speed_gauge.dart';
|
||||
Reference in New Issue
Block a user