From 6b72bc9509f7027e4c562c5fc8409def27a65acb Mon Sep 17 00:00:00 2001 From: LollipopKit <2036293523@qq.com> Date: Fri, 17 Sep 2021 00:28:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=B7=BB=E5=8A=A0=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=9C=8D=E5=8A=A1=E5=99=A8=E4=BF=A1=E6=81=AF=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E5=9C=A8=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E9=A1=B5=E6=98=BE=E7=A4=BACPU=E3=80=81=E5=86=85=E5=AD=98?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/extension/stringx.dart | 3 + lib/core/utils.dart | 37 +- lib/data/model/disk_info.dart | 46 +++ lib/data/model/server_private_info.dart | 57 +++ lib/data/model/server_status.dart | 90 +++++ lib/data/model/tcp_status.dart | 39 ++ lib/data/provider/server.dart | 29 ++ lib/data/store/server.dart | 26 ++ lib/locator.dart | 7 + lib/main.dart | 3 + lib/view/page/debug.dart | 13 +- lib/view/page/home.dart | 18 +- lib/view/page/server.dart | 455 +++++++++++++++++++----- lib/view/widget/card_dialog.dart | 25 ++ lib/view/widget/circle_pie.dart | 49 +-- 15 files changed, 759 insertions(+), 138 deletions(-) create mode 100644 lib/core/extension/stringx.dart create mode 100644 lib/data/model/disk_info.dart create mode 100644 lib/data/model/server_private_info.dart create mode 100644 lib/data/model/server_status.dart create mode 100644 lib/data/model/tcp_status.dart create mode 100644 lib/data/provider/server.dart create mode 100644 lib/view/widget/card_dialog.dart diff --git a/lib/core/extension/stringx.dart b/lib/core/extension/stringx.dart new file mode 100644 index 00000000..e7d42008 --- /dev/null +++ b/lib/core/extension/stringx.dart @@ -0,0 +1,3 @@ +extension StringX on String { + int get i => int.parse(this); +} diff --git a/lib/core/utils.dart b/lib/core/utils.dart index 97924745..38c93107 100644 --- a/lib/core/utils.dart +++ b/lib/core/utils.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:toolbox/core/persistant_store.dart'; +import 'package:toolbox/view/widget/card_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; void unawaited(Future future) {} @@ -24,19 +26,44 @@ void showSnackBarWithAction( Future openUrl(String url) async { print('openUrl $url'); - if (!await canLaunch(url)) { print('canLaunch false'); return false; } - final ok = await launch(url, forceSafariVC: false); - if (ok == true) { return true; } - print('launch $url failed'); - return false; } + +Future? showRoundDialog( + BuildContext context, String title, Widget child, List actions, + {EdgeInsets? padding}) { + return showDialog( + context: context, + builder: (ctx) { + return CardDialog( + title: Text(title), + content: child, + actions: actions, + padding: padding, + ); + }); +} + +Widget buildSwitch(BuildContext context, StoreProperty prop, + {Function(bool)? func}) { + return ValueListenableBuilder( + valueListenable: prop.listenable(), + builder: (context, bool value, widget) { + return Switch( + value: value, + onChanged: (value) { + if (func != null) func(value); + prop.put(value); + }); + }, + ); +} diff --git a/lib/data/model/disk_info.dart b/lib/data/model/disk_info.dart new file mode 100644 index 00000000..b8cc2c63 --- /dev/null +++ b/lib/data/model/disk_info.dart @@ -0,0 +1,46 @@ +class DiskInfo { +/* +{ + "mountPath": "", + "mountLocation": "", + "usedPercent": 0, + "used": "",= + "size": "", + "avail": "" +} +*/ + + String? mountPath; + String? mountLocation; + double? usedPercent; + String? used; + String? size; + String? avail; + + DiskInfo({ + this.mountPath, + this.mountLocation, + this.usedPercent, + this.used, + this.size, + this.avail, + }); + DiskInfo.fromJson(Map json) { + mountPath = json["mountPath"]?.toString(); + mountLocation = json["mountLocation"]?.toString(); + usedPercent = double.parse(json["usedPercent"]); + used = json["used"]?.toString(); + size = json["size"]?.toString(); + avail = json["avail"]?.toString(); + } + Map toJson() { + final Map data = {}; + data["mountPath"] = mountPath; + data["mountLocation"] = mountLocation; + data["usedPercent"] = usedPercent; + data["used"] = used; + data["size"] = size; + data["avail"] = avail; + return data; + } +} diff --git a/lib/data/model/server_private_info.dart b/lib/data/model/server_private_info.dart new file mode 100644 index 00000000..dcaf4b9d --- /dev/null +++ b/lib/data/model/server_private_info.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +/// +/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/ +/// +class ServerPrivateInfo { +/* +{ + "ip": "", + "port": 1, + "user": "", + "authorization": "" +} +*/ + + String? name; + String? ip; + int? port; + String? user; + Object? authorization; + + ServerPrivateInfo({ + this.name, + this.ip, + this.port, + this.user, + this.authorization, + }); + ServerPrivateInfo.fromJson(Map json) { + name = json["name"]?.toString(); + ip = json["ip"]?.toString(); + port = json["port"]?.toInt(); + user = json["user"]?.toString(); + authorization = json["authorization"]; + } + Map toJson() { + final Map data = {}; + data["name"] = name; + data["ip"] = ip; + data["port"] = port; + data["user"] = user; + data["authorization"] = authorization; + return data; + } +} + +List? getServerInfoList(dynamic data) { + List ss = []; + if (data is String) { + data = json.decode(data); + } + for (var t in data) { + ss.add(ServerPrivateInfo.fromJson(t)); + } + + return ss; +} diff --git a/lib/data/model/server_status.dart b/lib/data/model/server_status.dart new file mode 100644 index 00000000..ed6e7bb4 --- /dev/null +++ b/lib/data/model/server_status.dart @@ -0,0 +1,90 @@ +import 'package:toolbox/data/model/disk_info.dart'; +import 'package:toolbox/data/model/tcp_status.dart'; + +/// +/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/ +/// + +class ServerStatus { +/* +{ + "cpuPercent": 0, + "memList": [ + 1 + ], + "sysVer": "", + "uptime": "", + "disk": [ + { + "mountPath": "", + "mountLocation": "", + "usedPercent": 0, + "used": "", + "size": "", + "avail": "" + } + ] +} +*/ + + double? cpuPercent; + List? memList; + String? sysVer; + String? uptime; + List? disk; + TcpStatus? tcp; + + ServerStatus( + {this.cpuPercent, + this.memList, + this.sysVer, + this.uptime, + this.disk, + this.tcp}); + ServerStatus.fromJson(Map json) { + cpuPercent = double.parse(json["cpuPercent"]); + if (json["memList"] != null) { + final v = json["memList"]; + final arr0 = []; + v.forEach((v) { + arr0.add(v.toInt()); + }); + memList = arr0; + } + sysVer = json["sysVer"]?.toString(); + uptime = json["uptime"]?.toString(); + if (json["disk"] != null) { + final v = json["disk"]; + final arr0 = []; + v.forEach((v) { + arr0.add(DiskInfo.fromJson(v)); + }); + disk = arr0; + } + tcp = TcpStatus.fromJson(json['tcp']); + } + Map toJson() { + final Map data = {}; + data["cpuPercent"] = cpuPercent; + if (memList != null) { + final v = memList; + final arr0 = []; + for (var v in v!) { + arr0.add(v); + } + data["memList"] = arr0; + } + data["sysVer"] = sysVer; + data["uptime"] = uptime; + if (disk != null) { + final v = disk; + final arr0 = []; + for (var v in v!) { + arr0.add(v!.toJson()); + } + data["disk"] = arr0; + } + data['tcp'] = tcp; + return data; + } +} diff --git a/lib/data/model/tcp_status.dart b/lib/data/model/tcp_status.dart new file mode 100644 index 00000000..0e3440c9 --- /dev/null +++ b/lib/data/model/tcp_status.dart @@ -0,0 +1,39 @@ +/// +/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/ +/// +class TcpStatus { +/* +{ + "maxConn": 0, + "active": 1, + "passive": 2, + "fail": 3 +} +*/ + + int? maxConn; + int? active; + int? passive; + int? fail; + + TcpStatus({ + this.maxConn, + this.active, + this.passive, + this.fail, + }); + TcpStatus.fromJson(Map json) { + maxConn = json["maxConn"]?.toInt(); + active = json["active"]?.toInt(); + passive = json["passive"]?.toInt(); + fail = json["fail"]?.toInt(); + } + Map toJson() { + final Map data = Map(); + data["maxConn"] = maxConn; + data["active"] = active; + data["passive"] = passive; + data["fail"] = fail; + return data; + } +} diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart new file mode 100644 index 00000000..fbb7f8a9 --- /dev/null +++ b/lib/data/provider/server.dart @@ -0,0 +1,29 @@ +import 'package:toolbox/core/provider_base.dart'; +import 'package:toolbox/data/model/server_private_info.dart'; +import 'package:toolbox/data/store/server.dart'; +import 'package:toolbox/locator.dart'; + +class ServerProvider extends BusyProvider { + late List _servers; + + List get servers => _servers; + + Future loadData() async { + setBusyState(true); + _servers = locator().fetch(); + setBusyState(false); + notifyListeners(); + } + + void addServer(ServerPrivateInfo info) { + _servers.add(info); + locator().put(info); + notifyListeners(); + } + + void delServer(ServerPrivateInfo info) { + _servers.remove(info); + locator().delete(info); + notifyListeners(); + } +} diff --git a/lib/data/store/server.dart b/lib/data/store/server.dart index 8b137891..06568f21 100644 --- a/lib/data/store/server.dart +++ b/lib/data/store/server.dart @@ -1 +1,27 @@ +import 'dart:convert'; +import 'package:toolbox/core/persistant_store.dart'; +import 'package:toolbox/data/model/server_private_info.dart'; + +class ServerStore extends PersistentStore { + void put(ServerPrivateInfo info) { + final ss = fetch(); + if (!have(info)) ss.add(info); + box.put('servers', json.encode(ss)); + } + + List fetch() { + return getServerInfoList( + json.decode(box.get('servers', defaultValue: '[]')!))!; + } + + void delete(ServerPrivateInfo s) { + final ss = fetch(); + ss.removeWhere((e) => e.ip == s.ip && e.port == s.port && e.user == e.user); + box.put('servers', json.encode(ss)); + } + + bool have(ServerPrivateInfo s) => fetch() + .where((e) => e.ip == s.ip && e.port == s.port && e.user == e.user) + .isNotEmpty; +} diff --git a/lib/locator.dart b/lib/locator.dart index 8b009b83..800f26e6 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,7 +1,9 @@ import 'package:get_it/get_it.dart'; import 'package:toolbox/data/provider/app.dart'; import 'package:toolbox/data/provider/debug.dart'; +import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/service/app.dart'; +import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/setting.dart'; GetIt locator = GetIt.instance; @@ -13,12 +15,17 @@ void setupLocatorForServices() { void setupLocatorForProviders() { locator.registerSingleton(AppProvider()); locator.registerSingleton(DebugProvider()); + locator.registerSingleton(ServerProvider()); } Future setupLocatorForStores() async { final setting = SettingStore(); await setting.init(boxName: 'setting'); locator.registerSingleton(setting); + + final server = ServerStore(); + await server.init(boxName: 'server'); + locator.registerSingleton(server); } Future setupLocator() async { diff --git a/lib/main.dart b/lib/main.dart index 5e599cc0..54c7c9e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,11 +7,13 @@ import 'package:toolbox/app.dart'; import 'package:toolbox/core/analysis.dart'; import 'package:toolbox/data/provider/app.dart'; import 'package:toolbox/data/provider/debug.dart'; +import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/locator.dart'; Future initApp() async { await Hive.initFlutter(); await setupLocator(); + locator().loadData(); } void runInZone(dynamic Function() body) { @@ -51,6 +53,7 @@ Future main() async { providers: [ ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), + ChangeNotifierProvider(create: (_) => locator()), ], child: const MyApp(), ), diff --git a/lib/view/page/debug.dart b/lib/view/page/debug.dart index 9e2dcf05..0f8396b3 100644 --- a/lib/view/page/debug.dart +++ b/lib/view/page/debug.dart @@ -10,8 +10,6 @@ class DebugPage extends StatefulWidget { } class _DebugPageState extends State { - DebugProvider get debug => Provider.of(context); - @override Widget build(BuildContext context) { return Scaffold( @@ -33,11 +31,12 @@ class _DebugPageState extends State { fontWeight: FontWeight.bold, ), child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: debug.widgets, - ), + child: Consumer(builder: (_, debug, __) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: debug.widgets); + }), ), ), ); diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 1809accc..4b7a1718 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -1,6 +1,10 @@ +import 'package:after_layout/after_layout.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:toolbox/core/route.dart'; +import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/res/build_data.dart'; +import 'package:toolbox/locator.dart'; import 'package:toolbox/view/page/convert.dart'; import 'package:toolbox/view/page/debug.dart'; import 'package:toolbox/view/page/server.dart'; @@ -14,8 +18,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State - with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { - final List _tabs = ['服务器', '编/解码', '1', '2', '3']; + with + AutomaticKeepAliveClientMixin, + SingleTickerProviderStateMixin, + AfterLayoutMixin { + final List _tabs = ['服务器', '编/解码', '1', '2']; late final TabController _tabController; @override @@ -45,7 +52,6 @@ class _MyHomePageState extends State ConvertPage(), ConvertPage(), ConvertPage(), - ConvertPage() ]), ); } @@ -93,4 +99,10 @@ class _MyHomePageState extends State @override bool get wantKeepAlive => true; + + @override + Future afterFirstLayout(BuildContext context) async { + await GetIt.I.allReady(); + await locator().loadData(); + } } diff --git a/lib/view/page/server.dart b/lib/view/page/server.dart index abb70744..aeb6b2a8 100644 --- a/lib/view/page/server.dart +++ b/lib/view/page/server.dart @@ -1,9 +1,19 @@ +import 'package:after_layout/after_layout.dart'; import 'package:charts_flutter/flutter.dart' as chart; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:get_it/get_it.dart'; +import 'package:provider/provider.dart'; import 'package:ssh2/ssh2.dart'; +import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/utils.dart'; +import 'package:toolbox/data/model/disk_info.dart'; +import 'package:toolbox/data/model/server_private_info.dart'; +import 'package:toolbox/data/model/server_status.dart'; +import 'package:toolbox/data/model/tcp_status.dart'; +import 'package:toolbox/data/provider/server.dart'; +import 'package:toolbox/locator.dart'; import 'package:toolbox/view/widget/circle_pie.dart'; class ServerPage extends StatefulWidget { @@ -14,13 +24,29 @@ class ServerPage extends StatefulWidget { } class _ServerPageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, AfterLayoutMixin { late MediaQueryData _media; late ThemeData _theme; + bool useKey = false; + + final nameController = TextEditingController(); + final ipController = TextEditingController(); + final portController = TextEditingController(); + final usernameController = TextEditingController(); + final passwordController = TextEditingController(); + final keyController = TextEditingController(); + final ipFocusNode = FocusNode(); + final portFocusNode = FocusNode(); + final usernameFocusNode = FocusNode(); + final passwordFocusNode = FocusNode(); + + late ServerProvider serverProvider; + final cachedServerStatus = []; @override void initState() { super.initState(); + serverProvider = locator(); } @override @@ -38,123 +64,359 @@ class _ServerPageState extends State child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 7), child: AnimationLimiter( - child: Column( - children: AnimationConfiguration.toStaggeredList( - duration: const Duration(milliseconds: 377), - childAnimationBuilder: (widget) => SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: widget, + child: Consumer(builder: (_, pro, __) { + return Column( + children: AnimationConfiguration.toStaggeredList( + duration: const Duration(milliseconds: 377), + childAnimationBuilder: (widget) => SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: widget, + ), ), - ), - children: [const SizedBox(height: 13), ..._buildServerCards()], - ))), + children: [ + const SizedBox(height: 13), + ...pro.servers + .map((e) => _buildEachServerCard(e, pro.servers.indexOf(e))) + ], + )); + })), ), onTap: () => FocusScope.of(context).requestFocus(FocusNode()), ), floatingActionButton: FloatingActionButton( onPressed: () { - showSnackBar(context, const Text('')); + showRoundDialog(context, '新建服务器连接', _buildTextInputField(context), [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('关闭')), + TextButton( + onPressed: () { + final authorization = keyController.text.isEmpty + ? passwordController.text + : { + "privateKey": keyController.text, + "passphrase": passwordController.text + }; + serverProvider.addServer(ServerPrivateInfo( + name: nameController.text, + ip: ipController.text, + port: int.parse(portController.text), + user: usernameController.text, + authorization: authorization)); + nameController.clear(); + ipController.clear(); + portController.clear(); + usernameController.clear(); + passwordController.clear(); + keyController.clear(); + Navigator.of(context).pop(); + }, + child: const Text('连接')) + ]); }, tooltip: 'add a server', + heroTag: 'server page fab', child: const Icon(Icons.add), ), ); } - Future>? _getData() async { - final client = SSHClient( - host: '', - port: 0, - username: '', - passwordOrKey: '', + InputDecoration _buildDecoration(String label, {TextStyle? textStyle}) { + return InputDecoration(labelText: label, labelStyle: textStyle); + } + + Widget _buildTextInputField(BuildContext ctx) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: nameController, + keyboardType: TextInputType.text, + decoration: _buildDecoration('名称'), + onSubmitted: (_) => + FocusScope.of(context).requestFocus(ipFocusNode), + ), + TextField( + controller: ipController, + focusNode: ipFocusNode, + keyboardType: TextInputType.text, + decoration: _buildDecoration('IP'), + onSubmitted: (_) => + FocusScope.of(context).requestFocus(usernameFocusNode), + ), + TextField( + controller: portController, + focusNode: portFocusNode, + keyboardType: TextInputType.number, + decoration: _buildDecoration('Port'), + onSubmitted: (_) => + FocusScope.of(context).requestFocus(usernameFocusNode), + ), + TextField( + controller: usernameController, + focusNode: usernameFocusNode, + keyboardType: TextInputType.text, + decoration: _buildDecoration('用户名'), + onSubmitted: (_) => + FocusScope.of(context).requestFocus(passwordFocusNode), + ), + TextField( + controller: keyController, + keyboardType: TextInputType.text, + decoration: _buildDecoration('密钥(可选)'), + onSubmitted: (_) => {}, + ), + TextField( + controller: passwordController, + focusNode: passwordFocusNode, + obscureText: true, + keyboardType: TextInputType.text, + decoration: _buildDecoration('密码'), + onSubmitted: (_) => {}, + ), + ], + ), ); + } + + Future? _getData(ServerPrivateInfo info) async { + final client = SSHClient( + host: info.ip!, + port: info.port!, + username: info.user!, + passwordOrKey: info.authorization, + ); + await client.connect(); final cpu = await client.execute( "top -bn1 | grep load | awk '{printf \"%.2f\", \$(NF-2)}'") ?? - 'failed'; - final mem = await client - .execute("free -m | awk 'NR==2{printf \"%s/%sMB\", \$3,\$2}'") ?? - 'failed'; - return [cpu.trim(), mem.trim()]; + '0'; + final mem = await client.execute('free -m') ?? ''; + final sysVer = await client.execute('cat /etc/issue.net') ?? 'Unkown'; + final upTime = await client.execute('uptime') ?? 'Failed'; + final disk = await client.execute('df -h') ?? 'Failed'; + final tcp = await client.execute('cat /proc/net/snmp') ?? 'Failed'; + + return ServerStatus( + cpuPercent: double.parse(cpu.trim()), + memList: _getMem(mem), + sysVer: sysVer.trim(), + disk: _getDisk(disk), + uptime: _getUpTime(upTime), + tcp: _getTcp(tcp)); } - Widget _buildEachServerCard() { - return FutureBuilder>( - future: _getData(), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - return Text("Error: ${snapshot.error}"); - } else { - return _buildEachCardContent(snapshot); - } - } else { - return const CircularProgressIndicator(); - } + String _getUpTime(String raw) { + return raw.split('up ')[1].split(', ')[0]; + } + + TcpStatus _getTcp(String raw) { + final lines = raw.split('\n'); + int idx = 0; + for (var item in lines) { + if (item.contains('Tcp:')) { + idx++; + } + if (idx == 2) { + final vals = item.split(RegExp(r'\s{1,}')); + return TcpStatus( + maxConn: vals[5].i, + active: vals[6].i, + passive: vals[7].i, + fail: vals[8].i); + } + } + return TcpStatus(maxConn: 0, active: 0, passive: 0, fail: 0); + } + + List _getDisk(String disk) { + final list = []; + final items = disk.split('\n'); + for (var item in items) { + if (items.indexOf(item) == 0 || item.isEmpty) { + continue; + } + final vals = item.split(RegExp(r'\s{1,}')); + list.add(DiskInfo( + mountPath: vals[1], + mountLocation: vals[5], + usedPercent: double.parse(vals[4].replaceFirst('%', '')), + used: vals[2], + size: vals[1], + avail: vals[3])); + } + return list; + } + + List _getMem(String mem) { + for (var item in mem.split('\n')) { + if (item.contains('Mem:')) { + return RegExp(r'[1-9][0-9]*') + .allMatches(item) + .map((e) => int.parse(item.substring(e.start, e.end))) + .toList(); + } + } + return []; + } + + Widget _buildEachServerCard(ServerPrivateInfo e, int index) { + return FutureBuilder( + future: _getData(e), + builder: (BuildContext context, AsyncSnapshot snapshot) { + return GestureDetector( + child: _buildEachCardContent(snapshot, e.name ?? '', index), + onLongPress: () => + showRoundDialog(context, '是否删除', const Text('删除后无法恢复'), [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('否')), + TextButton( + onPressed: () { + serverProvider.delServer(e); + Navigator.of(context).pop(); + }, + child: const Text('是')) + ]), + ); }, ); } - Widget _buildEachCardContent(AsyncSnapshot snapshot) { - final cpuPercent = double.parse(snapshot.data![0]) * 100; - final memSplit = snapshot.data![1].replaceFirst('MB', '').split('/'); - final memPercent = int.parse(memSplit[0]) / int.parse(memSplit[1]) * 100; - final cpuData = [ - IndexPercent(0, cpuPercent.toInt()), - ]; - final memData = [ - IndexPercent(0, memPercent.toInt()), - ]; - return Card( - child: Padding(padding:const EdgeInsets.all(13) , child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _buildEachCardContent( + AsyncSnapshot snapshot, String serverName, int index) { + Widget child; + if (snapshot.connectionState != ConnectionState.done) { + if (cachedServerStatus.length > index && cachedServerStatus.elementAt(index) != null) { + child = _buildRealServerCard(cachedServerStatus.elementAt(index)!, serverName); + } else { + child = _buildRealServerCard( + ServerStatus( + cpuPercent: 0, + memList: [100, 0], + disk: [ + DiskInfo( + mountLocation: '', + mountPath: '', + used: '', + size: '', + avail: '', + usedPercent: 0) + ], + sysVer: '', + uptime: '', + tcp: TcpStatus(maxConn: 0, active: 0, passive: 0, fail: 0)), + serverName); + } + } else if (snapshot.hasError) { + child = Column( children: [ - Text(' Jilin', style: TextStyle(fontWeight: FontWeight.bold),), - const SizedBox(height: 7,), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildPercentCircle(cpuPercent, 'CPU', [ - chart.Series( - id: 'CPU', - domainFn: (IndexPercent cpu, _) => cpu.id, - measureFn: (IndexPercent cpu, _) => cpu.percent, - data: cpuData, + Text( + serverName, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Center( + child: Text("Error: ${snapshot.error}"), ) - ]), - _buildPercentCircle(memPercent, 'MEM', [ - chart.Series( - id: 'MEM', - domainFn: (IndexPercent sales, _) => sales.id, - measureFn: (IndexPercent sales, _) => sales.percent, - data: memData, - ) - ]) - ], - ) ], - ),), + ); + } else { + if (cachedServerStatus.length <= index) { + cachedServerStatus.add(snapshot.data!); + } else { + cachedServerStatus[index] = snapshot.data!; + } + child = _buildRealServerCard(snapshot.data!, serverName); + } + return Card( + child: Padding( + padding: const EdgeInsets.all(13), + child: child, + ), ); } - Widget _buildPercentCircle( - double percent, String title, List> series) { + Widget _buildRealServerCard(ServerStatus ss, String serverName) { + final cpuData = [ + IndexPercent(0, ss.cpuPercent!.toInt()), + IndexPercent(1, 100 - ss.cpuPercent!.toInt()), + ]; + final memData = []; + for (var e in ss.memList!) { + memData.add(IndexPercent(ss.memList!.indexOf(e), e!.toInt())); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + serverName, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(ss.uptime!, + style: TextStyle( + color: _theme.textTheme.bodyText1!.color!.withAlpha(100))) + ], + ), + const SizedBox( + height: 13, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildPercentCircle(ss.cpuPercent!, 'CPU', [ + chart.Series( + id: 'CPU', + domainFn: (IndexPercent cpu, _) => cpu.id, + measureFn: (IndexPercent cpu, _) => cpu.percent, + data: cpuData, + ) + ]), + _buildPercentCircle( + ss.memList![1]! / ss.memList![0]! * 100, 'Mem', [ + chart.Series( + id: 'Mem', + domainFn: (IndexPercent sales, _) => sales.id, + measureFn: (IndexPercent sales, _) => sales.percent, + data: memData, + ) + ]), + _buildIOData('Net', ss.tcp!.maxConn!.toString(), '0kb/s'), + _buildIOData('Disk', '0kb/s', '0kb/s') + ], + ) + ], + ); + } + + Widget _buildIOData(String title, String up, String down) { return SizedBox( width: _media.size.width * 0.2, height: _media.size.height * 0.1, child: Stack( children: [ - DonutPieChart.withRandomData(), Positioned( - child: Text( - '${percent.toStringAsFixed(1)}%', - textAlign: TextAlign.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '↓$up', + textAlign: TextAlign.start, + ), + Text( + '↑$down', + textAlign: TextAlign.center, + ) + ], ), + top: _media.size.height * 0.012, left: 0, right: 0, - top: 0, - bottom: 0 ), Positioned( child: Text(title, textAlign: TextAlign.center), @@ -166,10 +428,39 @@ class _ServerPageState extends State ); } - List _buildServerCards() { - return [_buildEachServerCard()]; + Widget _buildPercentCircle(double percent, String title, + List> series) { + return SizedBox( + width: _media.size.width * 0.2, + height: _media.size.height * 0.1, + child: Stack( + children: [ + DonutPieChart(series), + Positioned( + child: Text( + '${percent.toStringAsFixed(1)}%', + textAlign: TextAlign.center, + ), + left: 0, + right: 0, + top: _media.size.height * 0.03, + ), + Positioned( + child: Text(title, textAlign: TextAlign.center), + bottom: 0, + left: 0, + right: 0) + ], + ), + ); } @override bool get wantKeepAlive => true; + + @override + Future afterFirstLayout(BuildContext context) async { + await GetIt.I.allReady(); + await locator().loadData(); + } } diff --git a/lib/view/widget/card_dialog.dart b/lib/view/widget/card_dialog.dart new file mode 100644 index 00000000..b4d34819 --- /dev/null +++ b/lib/view/widget/card_dialog.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class CardDialog extends StatelessWidget { + const CardDialog( + {Key? key, this.title, this.content, this.actions, this.padding}) + : super(key: key); + + final Widget? content; + final List? actions; + final Widget? title; + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: padding ?? const EdgeInsets.fromLTRB(24, 17, 24, 7), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)), + ), + title: title, + content: content, + actions: actions, + ); + } +} diff --git a/lib/view/widget/circle_pie.dart b/lib/view/widget/circle_pie.dart index a591b75f..556495cd 100644 --- a/lib/view/widget/circle_pie.dart +++ b/lib/view/widget/circle_pie.dart @@ -1,5 +1,3 @@ -import 'dart:math'; -// EXCLUDE_FROM_GALLERY_DOCS_END import 'package:charts_flutter/flutter.dart' as charts; import 'package:flutter/material.dart'; @@ -7,59 +5,28 @@ class DonutPieChart extends StatelessWidget { final List> seriesList; final bool animate; - const DonutPieChart(this.seriesList, {Key? key, this.animate = true}) : super(key: key); - - factory DonutPieChart.withRandomData() { - return DonutPieChart(_createRandomData()); - } - - /// Create random data. - static List> _createRandomData() { - final random = Random(); - - final data = [ - IndexPercent(0, random.nextInt(100)), - IndexPercent(1, random.nextInt(100)), - IndexPercent(2, random.nextInt(100)), - IndexPercent(3, random.nextInt(100)), - ]; - - return [ - charts.Series( - id: 'Sales', - domainFn: (IndexPercent sales, _) => sales.id, - measureFn: (IndexPercent sales, _) => sales.percent, - data: data, - ) - ]; - } - // EXCLUDE_FROM_GALLERY_DOCS_END + const DonutPieChart(this.seriesList, {Key? key, this.animate = false}) + : super(key: key); @override Widget build(BuildContext context) { return charts.PieChart(seriesList, animate: animate, layoutConfig: charts.LayoutConfig( - leftMarginSpec: charts.MarginSpec.fixedPixel(1), - topMarginSpec: charts.MarginSpec.fixedPixel(1), - rightMarginSpec: charts.MarginSpec.fixedPixel(1), - bottomMarginSpec: charts.MarginSpec.fixedPixel(17) - ), - // Configure the width of the pie slices to 60px. The remaining space in - // the chart will be left as a hole in the center. + leftMarginSpec: charts.MarginSpec.fixedPixel(1), + topMarginSpec: charts.MarginSpec.fixedPixel(1), + rightMarginSpec: charts.MarginSpec.fixedPixel(1), + bottomMarginSpec: charts.MarginSpec.fixedPixel(17)), defaultRenderer: charts.ArcRendererConfig( arcWidth: 6, - minHoleWidthForCenterContent: 60, arcRatio: 0.2, - ) - ); + )); } } -/// Sample linear data type. class IndexPercent { final int id; final int percent; IndexPercent(this.id, this.percent); -} \ No newline at end of file +}