diff --git a/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingCommPlugin.java b/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingCommPlugin.java index 5ca96df..373524a 100644 --- a/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingCommPlugin.java +++ b/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingCommPlugin.java @@ -1,6 +1,7 @@ package cn.zhios.zhiying_comm; import androidx.annotation.NonNull; + import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -8,32 +9,34 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; -/** ZhiyingCommPlugin */ +/** + * ZhiyingCommPlugin + */ public class ZhiyingCommPlugin implements FlutterPlugin { - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { // final MethodChannel channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "zhiying_comm"); // channel.setMethodCallHandler(new ZhiyingCommPlugin()); - ZhiyingFlutterCommNative.getInstance().registerWith(flutterPluginBinding.getFlutterEngine().getDartExecutor()); - } + ZhiyingFlutterCommNative.getInstance().registerWith(flutterPluginBinding.getFlutterEngine().getDartExecutor()); + } - // This static function is optional and equivalent to onAttachedToEngine. It supports the old - // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting - // plugin registration via this function while apps migrate to use the new Android APIs - // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. - // - // It is encouraged to share logic between onAttachedToEngine and registerWith to keep - // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called - // depending on the user's project. onAttachedToEngine or registerWith must both be defined - // in the same class. - public static void registerWith(Registrar registrar) { + // This static function is optional and equivalent to onAttachedToEngine. It supports the old + // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting + // plugin registration via this function while apps migrate to use the new Android APIs + // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. + // + // It is encouraged to share logic between onAttachedToEngine and registerWith to keep + // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called + // depending on the user's project. onAttachedToEngine or registerWith must both be defined + // in the same class. + public static void registerWith(Registrar registrar) { // final MethodChannel channel = new MethodChannel(registrar.messenger(), "zhiying_comm"); // channel.setMethodCallHandler(new ZhiyingCommPlugin()); - ZhiyingFlutterCommNative.getInstance().registerWith(registrar.messenger()); - } + ZhiyingFlutterCommNative.getInstance().registerWith(registrar.messenger()); + } - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - } + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + } } diff --git a/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingFlutterCommNative.java b/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingFlutterCommNative.java index 0585adf..a895905 100644 --- a/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingFlutterCommNative.java +++ b/android/src/main/java/cn/zhios/zhiying_comm/ZhiyingFlutterCommNative.java @@ -67,6 +67,7 @@ public class ZhiyingFlutterCommNative implements MethodCallHandler { Map map = new HashMap(); map.put("success", "1"); result.success(map); + return; } /* 跳转原生页面(非公共跳转) */ @@ -77,6 +78,7 @@ public class ZhiyingFlutterCommNative implements MethodCallHandler { Map map = new HashMap(); map.put("success", "1"); result.success(map); + return; } /* 获取设置 */ @@ -86,6 +88,7 @@ public class ZhiyingFlutterCommNative implements MethodCallHandler { map = nat.getSetting(); } result.success(map); + return; } /* 调用原生方法 */ @@ -100,10 +103,14 @@ public class ZhiyingFlutterCommNative implements MethodCallHandler { }); } else { result.success(ZhiyingFlutterCommNativeResult.notImp().toMap()); + return; } + } else { result.notImplemented(); + return; } + } } diff --git a/lib/util/empty_util.dart b/lib/util/empty_util.dart new file mode 100644 index 0000000..22691df --- /dev/null +++ b/lib/util/empty_util.dart @@ -0,0 +1,19 @@ +/// 判空工具类 +class EmptyUtil { + + /// 判断是否为空,object的类型可以为:String Map List + static bool isEmpty(Object object) { + if (null == object) return true; + if (object is String && object.isEmpty) { + return true; + } + if (object is List && object.isEmpty) { + return true; + } + + if (object is Map && object.isEmpty) { + return true; + } + return false; + } +} diff --git a/lib/util/encode_util.dart b/lib/util/encode_util.dart index 6357686..0cff747 100644 --- a/lib/util/encode_util.dart +++ b/lib/util/encode_util.dart @@ -1,7 +1,6 @@ import 'package:crypto/crypto.dart'; import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter_native_image/flutter_native_image.dart'; class EncodeUtil { diff --git a/lib/util/global_config.dart b/lib/util/global_config.dart new file mode 100644 index 0000000..b918aa6 --- /dev/null +++ b/lib/util/global_config.dart @@ -0,0 +1,8 @@ +class GlobalConfig { + /// ====================== 后端json返回的key ====================== /// + static final String HTTP_RESPONSE_KEY_CODE = 'code'; + static final String HTTP_RESPONSE_KEY_MSG = 'msg'; + static final String HTTP_RESPONSE_KEY_DATA = 'data'; + /// 成功返回的CODE值 + static final int RESPONSE_SUCCESS_CODE = 200; +} diff --git a/lib/util/log/let_log.dart b/lib/util/log/let_log.dart new file mode 100644 index 0000000..d3fb822 --- /dev/null +++ b/lib/util/log/let_log.dart @@ -0,0 +1,362 @@ +library let_log; + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +part 'log_widget.dart'; +part 'net_widget.dart'; + +enum _Type { log, debug, warn, error } +List _printNames = ["😄", "🐛", "❗", "❌", "⬆️", "⬇️"]; +List _tabNames = ["[Log]", "[Debug]", "[Warn]", "[Error]"]; +final RegExp _tabReg = RegExp(r"\[|\]"); + +String _getTabName(int index) { + return _tabNames[index].replaceAll(_tabReg, ""); +} + +class _Config { + /// Whether to display the log in reverse order + bool reverse = false; + + /// Whether or not to print logs in the ide + bool printNet = true; + + /// Whether or not to print net logs in the ide + bool printLog = true; + + /// Maximum number of logs, larger than this number, will be cleaned up, default value 500 + int maxLimit = 500; + + /// Set the names in ide print. + void setPrintNames({ + String log, + String debug, + String warn, + String error, + String request, + String response, + }) { + _printNames = [ + log ?? "[Log]", + debug ?? "[Debug]", + warn ?? "[Warn]", + error ?? "[Error]", + request ?? "[Req]", + response ?? "[Res]", + ]; + } + + /// Set the names in the app. + void setTabNames({ + String log, + String debug, + String warn, + String error, + String request, + String response, + }) { + _tabNames = [ + log ?? "[Log]", + debug ?? "[Debug]", + warn ?? "[Warn]", + error ?? "[Error]", + ]; + } +} + +class Logger extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const TabBar( + tabs: [ + Tab(child: Text("Log")), + Tab(child: Text("Net")), + ], + ), + elevation: 0, + ), + body: const TabBarView( + children: [ + LogWidget(), + NetWidget(), + ], + ), + ), + ); + } + + static bool enabled = true; + static _Config config = _Config(); + + /// Logging + static void log(Object message, [Object detail]) { + if (enabled) _Log.add(_Type.log, message, detail); + } + + /// Record debug information + static void debug(Object message, [Object detail]) { + if (enabled) _Log.add(_Type.debug, message, detail); + } + + /// Record warnning information + static void warn(Object message, [Object detail]) { + if (enabled) _Log.add(_Type.warn, message, detail); + } + + /// Record error information + static void error(Object message, [Object detail]) { + if (enabled) _Log.add(_Type.error, message, detail); + } + + /// Start recording time + static void time(Object key) { + assert(key != null); + if (enabled) _Log.time(key); + } + + /// End of record time + static void endTime(Object key) { + assert(key != null); + if (enabled) _Log.endTime(key); + } + + /// Clearance log + static void clear() { + _Log.clear(); + } + + /// Recording network information + static void net(String api, + {String type = "请求接口", int status = 100, Object data}) { + assert(api != null); + if (enabled) _Net.request(api, type, status, data); + } + + /// End of record network information, with statistics on duration and size. + static void endNet(String api, + {int status = 200, Object data, Object headers, String type}) { + assert(api != null); + if (enabled) _Net.response(api, status, data, headers, type); + } +} + +class _Log { + static final List<_Log> list = []; + static final ValueNotifier length = ValueNotifier(0); + static final Map _map = {}; + + final _Type type; + final String message; + final String detail; + final DateTime start; + const _Log({this.type, this.message, this.detail, this.start}); + + String get typeName { + return _printNames[type.index]; + } + + String get tabName { + return _tabNames[type.index]; + } + + bool contains(String keyword) { + if (keyword.isEmpty) return true; + return message != null && message.contains(keyword) || + detail != null && detail.contains(keyword); + } + + @override + String toString() { + final StringBuffer sb = StringBuffer(); + sb.writeln("Message: $message"); + sb.writeln("Time: $start"); + if (detail != null && detail.length > 100) { + sb.writeln("Detail: "); + sb.writeln(detail); + } else { + sb.writeln("Detail: $detail"); + } + + return sb.toString(); + } + + static void add(_Type type, Object value, Object detail) { + final log = _Log( + type: type, + message: value?.toString(), + detail: detail?.toString(), + start: DateTime.now(), + ); + list.add(log); + _clearWhenTooMuch(); + length.value++; + if (Logger.config.printLog) { + debugPrint( + "${log.typeName} ${log.message}${log.detail == null ? '' : '\n${log.detail}'}\n--------------------------------"); + } + } + + static void _clearWhenTooMuch() { + if (list.length > Logger.config.maxLimit) { + list.removeRange(0, (Logger.config.maxLimit * 0.2).ceil()); + } + } + + static void time(Object key) { + _map[key] = DateTime.now(); + } + + static void endTime(Object key) { + final data = _map[key]; + if (data != null) { + _map.remove(key); + final spend = DateTime.now().difference(data).inMilliseconds; + _Log.add(_Type.log, '$key: $spend ms', null); + } + } + + static void clear() { + list.clear(); + _map.clear(); + length.value = 0; + } +} + +class _Net extends ChangeNotifier { + static const all = "All"; + static final List<_Net> list = []; + static final ValueNotifier length = ValueNotifier(0); + static final Map _map = {}; + static final List types = [all]; + static final ValueNotifier typeLength = ValueNotifier(1); + + final String api; + final String req; + final DateTime start; + String type; + int status = 100; + int spend = 0; + String res; + String headers; + bool showDetail = false; + int _reqSize = -1; + int _resSize = -1; + + _Net({ + this.api, + this.type, + this.req, + this.headers, + this.start, + this.res, + this.spend = 0, + this.status = 100, + }); + + int getReqSize() { + if (_reqSize > -1) return _reqSize; + if (req != null && req.isNotEmpty) { + try { + return _reqSize = utf8.encode(req).length; + } catch (e) { + // print(e); + } + } + return 0; + } + + int getResSize() { + if (_resSize > -1) return _resSize; + if (res != null && res.isNotEmpty) { + try { + return _resSize = utf8.encode(res).length; + } catch (e) { + // print(e); + } + } + return 0; + } + + bool contains(String keyword) { + if (keyword.isEmpty) return true; + return api.contains(keyword) || + req != null && req.contains(keyword) || + res != null && res.contains(keyword); + } + + @override + String toString() { + final StringBuffer sb = StringBuffer(); + sb.writeln("[$status] $api"); + sb.writeln("Start: $start"); + sb.writeln("Spend: $spend ms"); + sb.writeln("Headers: $headers"); + sb.writeln("Request: $req"); + sb.writeln("Response: $res"); + return sb.toString(); + } + + static void request(String api, String type, int status, Object data) { + final net = _Net( + api: api, + type: type, + status: status, + req: data?.toString(), + start: DateTime.now(), + ); + list.add(net); + _map[api] = net; + if (type != null && type != "" && !types.contains(type)) { + types.add(type); + typeLength.value++; + } + _clearWhenTooMuch(); + length.value++; + if (Logger.config.printNet) { + debugPrint( + "${_printNames[4]} ${type == null ? '' : '$type: '}${net.api}${net.req == null ? '' : '\n${_printNames[0]} 请求数据: ${net.req}'}\n--------------------------------"); + } + } + + static void _clearWhenTooMuch() { + if (list.length > Logger.config.maxLimit) { + list.removeRange(0, (Logger.config.maxLimit * 0.2).ceil()); + } + } + + static void response( + String api, int status, Object data, Object headers, String type) { + _Net net = _map[api]; + if (net != null) { + _map.remove(net); + net.spend = DateTime.now().difference(net.start).inMilliseconds; + net.status = status; + net.headers = headers?.toString(); + net.res = data?.toString(); + length.notifyListeners(); + } else { + net = _Net(api: api, start: DateTime.now(), type: type); + net.status = status; + net.headers = headers?.toString(); + net.res = data?.toString(); + list.add(net); + _clearWhenTooMuch(); + length.value++; + } + if (Logger.config.printNet) { + debugPrint( + "${_printNames[5]} ${net.type == null ? '' : '${net.type}: '}${net.api}${net.res == null ? '' : '\n${_printNames[0]} 响应数据: ${net.res}'}\nSpend: ${net.spend} ms\n--------------------------------"); + } + } + + static void clear() { + list.clear(); + _map.clear(); + length.value = 0; + } +} diff --git a/lib/util/log/log_widget.dart b/lib/util/log/log_widget.dart new file mode 100644 index 0000000..2838588 --- /dev/null +++ b/lib/util/log/log_widget.dart @@ -0,0 +1,246 @@ +part of let_log; + +class LogWidget extends StatefulWidget { + const LogWidget({Key key}) : super(key: key); + + @override + _LogWidgetState createState() => _LogWidgetState(); +} + +class _LogWidgetState extends State { + bool _showSearch = false; + String _keyword = ""; + TextEditingController _textController; + ScrollController _scrollController; + FocusNode _focusNode; + bool _goDown = true; + + @override + void initState() { + _textController = TextEditingController(text: _keyword); + _scrollController = ScrollController(); + _focusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTools(), + Expanded( + child: ValueListenableBuilder( + valueListenable: _Log.length, + builder: (context, value, child) { + List<_Log> logs = _Log.list; + if (_selectTypes.length < 4 || _keyword.isNotEmpty) { + logs = _Log.list.where((test) { + return _selectTypes.contains(test.type) && + test.contains(_keyword); + }).toList(); + } + + final len = logs.length; + return ListView.separated( + itemBuilder: (context, index) { + final item = Logger.config.reverse + ? logs[len - index - 1] + : logs[index]; + final color = _getColor(item.type, context); + return _buildItem(item, color); + }, + itemCount: len, + controller: _scrollController, + separatorBuilder: (context, index) { + return const Divider( + height: 10, + thickness: 0.5, + color: Color(0xFFE0E0E0), + ); + }, + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + if (_goDown) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent * 2, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + } else { + _scrollController.animateTo( + 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + } + _goDown = !_goDown; + setState(() {}); + }, + mini: true, + child: Icon( + _goDown ? Icons.arrow_downward : Icons.arrow_upward, + ), + ), + ); + } + + Widget _buildItem(_Log item, Color color) { + return InkWell( + onTap: () { + final ClipboardData data = ClipboardData(text: item.toString()); + Clipboard.setData(data); + showDialog( + context: context, + barrierDismissible: true, + builder: (context) { + return const Center( + child: Material( + color: Colors.transparent, + child: Text( + "copy success!", + style: TextStyle(color: Colors.white, fontSize: 30), + ), + ), + ); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${item.tabName} ${item.message} (${item.start.hour}:${item.start.minute}:${item.start.second}:${item.start.millisecond})", + style: TextStyle(fontSize: 16, color: color), + ), + if (item.detail != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + item.detail, + style: TextStyle(fontSize: 14, color: color), + overflow: TextOverflow.ellipsis, + maxLines: 20, + ), + ) + ], + ), + ), + ); + } + + Color _getColor(_Type type, BuildContext context) { + switch (type) { + case _Type.debug: + return Colors.blue; + case _Type.warn: + return const Color(0xFFF57F17); + case _Type.error: + return Colors.red; + default: + return Theme.of(context).textTheme.body1.color; + } + } + + final List<_Type> _selectTypes = [ + _Type.log, + _Type.debug, + _Type.warn, + _Type.error + ]; + + Widget _buildTools() { + final List arr = []; + _Type.values.forEach((f) { + arr.add( + ChoiceChip( + label: Text( + _getTabName(f.index), + style: const TextStyle(fontSize: 14), + ), + selectedColor: const Color(0xFFCBE2F6), + selected: _selectTypes.contains(f), + onSelected: (value) { + _selectTypes.contains(f) + ? _selectTypes.remove(f) + : _selectTypes.add(f); + setState(() {}); + }, + ), + ); + }); + return Padding( + padding: const EdgeInsets.fromLTRB(16, 5, 0, 5), + child: AnimatedCrossFade( + crossFadeState: + _showSearch ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + firstChild: Row( + children: [ + Expanded( + child: Wrap( + spacing: 5, + children: arr, + ), + ), + const IconButton( + icon: Icon(Icons.clear), + onPressed: _Log.clear, + ), + IconButton( + icon: _keyword.isEmpty + ? const Icon(Icons.search) + : const Icon(Icons.filter_1), + onPressed: () { + _showSearch = true; + setState(() {}); + _focusNode.requestFocus(); + }, + ), + ], + ), + secondChild: Row( + children: [ + Expanded( + child: SizedBox( + height: 36, + child: TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.all(6), + ), + controller: _textController, + focusNode: _focusNode, + ), + ), + ), + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + _showSearch = false; + _keyword = _textController.text; + setState(() {}); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/util/log/net_widget.dart b/lib/util/log/net_widget.dart new file mode 100644 index 0000000..1e639ea --- /dev/null +++ b/lib/util/log/net_widget.dart @@ -0,0 +1,321 @@ +part of let_log; + +class NetWidget extends StatefulWidget { + const NetWidget({Key key}) : super(key: key); + + @override + _NetWidgetState createState() => _NetWidgetState(); +} + +class _NetWidgetState extends State { + bool _showSearch = false; + String _keyword = ""; + TextEditingController _textController; + ScrollController _scrollController; + FocusNode _focusNode; + bool _goDown = true; + + @override + void initState() { + _textController = TextEditingController(text: _keyword); + _scrollController = ScrollController(); + _focusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: _Net.typeLength, + builder: (context, value, child) { + return _buildTools(); + }, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _Net.length, + builder: (context, value, child) { + List<_Net> logs = _Net.list; + if (!_selectTypes.contains(_Net.all)) { + logs = _Net.list.where((test) { + return _selectTypes.contains(test.type) && + test.contains(_keyword); + }).toList(); + } else if (_keyword.isNotEmpty) { + logs = _Net.list.where((test) { + return test.contains(_keyword); + }).toList(); + } + + final len = logs.length; + return ListView.separated( + itemBuilder: (context, index) { + final item = Logger.config.reverse + ? logs[len - index - 1] + : logs[index]; + return _buildItem(item, context); + }, + itemCount: len, + controller: _scrollController, + reverse: Logger.config.reverse, + separatorBuilder: (context, index) { + return const Divider( + height: 10, + thickness: 0.5, + color: Color(0xFFE0E0E0), + ); + }, + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + if (_goDown) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent * 2, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + } else { + _scrollController.animateTo( + 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + } + _goDown = !_goDown; + setState(() {}); + }, + mini: true, + child: Icon( + _goDown ? Icons.arrow_downward : Icons.arrow_upward, + ), + ), + ); + } + + Widget _buildItem(_Net item, context) { + final color = _getColor(item.status); + return InkWell( + onTap: () { + item.showDetail = !item.showDetail; + setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "[${item.type}] ${item.api}", + style: const TextStyle(fontSize: 16), + ), + if (item.showDetail) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + "Request: ${item.req ?? ""}", + maxLines: 100, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + if (item.showDetail) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + "Response: ${item.res ?? ""}", + maxLines: 100, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + if (item.showDetail && item.headers != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + "Headers: ${item.headers ?? ""}", + maxLines: 100, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: Row( + children: [ + SizedBox( + width: 120, + child: Text( + "${item.start.hour}:${item.start.minute}:${item.start.second}:${item.start.millisecond}", + style: const TextStyle(fontSize: 14), + maxLines: 1, + ), + ), + SizedBox( + width: 100, + child: Text( + "${item.spend ?? "0"} ms", + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.visible, + maxLines: 1, + ), + ), + Text( + "${item.getReqSize()}/${item.getResSize()}B", + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.visible, + maxLines: 1, + ), + ], + ), + ) + ], + ), + ), + InkWell( + onTap: () { + final ClipboardData data = ClipboardData(text: item.toString()); + Clipboard.setData(data); + showDialog( + context: context, + barrierDismissible: true, + builder: (context) { + return const Center( + child: Material( + color: Colors.transparent, + child: Text( + "copy success!", + style: TextStyle(color: Colors.white, fontSize: 30), + ), + ), + ); + }, + ); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + item.status.toString(), + style: TextStyle(fontSize: 20, color: color), + ), + if (item.showDetail) + const Text( + "copy", + style: TextStyle(fontSize: 16, color: Colors.blue), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Color _getColor(int status) { + if (status == 200 || status == 0) { + return Colors.green; + } else if (status < 200) { + return Colors.blue; + } else { + return Colors.red; + } + } + + final List _selectTypes = [_Net.all]; + + Widget _buildTools() { + final List arr = []; + _Net.types.forEach((f) { + arr.add( + ChoiceChip( + label: Text(f, style: const TextStyle(fontSize: 14)), + selectedColor: const Color(0xFFCBE2F6), + selected: _selectTypes.contains(f), + onSelected: (value) { + _selectTypes.contains(f) + ? _selectTypes.remove(f) + : _selectTypes.add(f); + setState(() {}); + }, + ), + ); + }); + return Padding( + padding: const EdgeInsets.fromLTRB(16, 5, 0, 5), + child: AnimatedCrossFade( + crossFadeState: + _showSearch ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + firstChild: Row( + children: [ + Expanded( + child: Wrap( + spacing: 5, + children: arr, + ), + ), + const IconButton( + icon: Icon(Icons.clear), + onPressed: _Net.clear, + ), + IconButton( + icon: _keyword.isEmpty + ? const Icon(Icons.search) + : const Icon(Icons.filter_1), + onPressed: () { + _showSearch = true; + setState(() {}); + _focusNode.requestFocus(); + }, + ), + ], + ), + secondChild: Row( + children: [ + Expanded( + child: SizedBox( + height: 36, + child: TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.all(6), + ), + controller: _textController, + focusNode: _focusNode, + ), + ), + ), + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + _showSearch = false; + _keyword = _textController.text; + setState(() {}); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/util/native_util.dart b/lib/util/native_util.dart index 5cd8ab9..1187e38 100644 --- a/lib/util/native_util.dart +++ b/lib/util/native_util.dart @@ -25,8 +25,7 @@ class NativeUtil { {Map params}) async { var res; try { - res = - await NativeUtil.instance._methodChannel.invokeMethod(method, params); + res = await NativeUtil.instance._methodChannel.invokeMethod(method, params); } catch (e) { res = {'Failed': e.message}; } @@ -63,15 +62,22 @@ class NativeUtil { }); } - // 跳转原生商品详情 - static openGoodsDetail(Map params) async { - NativeUtil._invokeChannel('openGoodsDetail', params: params); - } static Future> getSetting() async { return NativeUtil._invokeChannel('getSetting'); } + +// static Future> onPagePop(bool isTop) async { +// NativeUtil._invokeChannel('pageDidPop', +// params: new Map.from({'isTop': isTop})); +// } +// +// static Future> onPagePush(bool isTop) async { +// NativeUtil._invokeChannel('pageDidPush', +// params: new Map.from({'isTop': isTop})); +// } + static Future> invokeMethod( String method, Map params) { Map request = {"method": method, "params": params}; diff --git a/lib/util/net_util.dart b/lib/util/net_util.dart index 2bc9bce..563cecc 100644 --- a/lib/util/net_util.dart +++ b/lib/util/net_util.dart @@ -1,25 +1,29 @@ import 'package:dio/dio.dart'; import 'package:dio/adapter.dart'; +import 'package:zhiying_comm/util/empty_util.dart'; import 'dart:io'; import 'dart:ui'; import 'dart:convert'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; import 'package:zhiying_comm/zhiying_comm.dart'; import 'package:package_info/package_info.dart'; import 'package:device_info/device_info.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'global_config.dart'; +import 'shared_prefe_util.dart'; +import 'time_util.dart'; + typedef OnSuccess = dynamic Function(dynamic data); typedef OnError = dynamic Function(String e); +typedef OnCache = dynamic Function(dynamic data); class NetUtil { - static final String APPKEY = "123"; Dio _dio; - static String _apiVersion; - NetUtil._() {} + NetUtil._(); + static NetUtil _instance; + static NetUtil getInstance() { if (_instance == null) { _instance = NetUtil._(); @@ -31,14 +35,14 @@ class NetUtil { if (_dio == null) { var setting = await NativeUtil.getSetting(); String domain = setting['domain']; //'http://www.hairuyi.com/'; - _config(domain, apiVersion: _apiVersion); + _config(domain); } return _dio; } - // 配置网络请求,基础地址,代理地址,如:192.168.0.123:8888(代理地址生产环境无效) - // apiVersion 接口版本 - void _config(String baseUrl, {String proxyUrl, String apiVersion}) { + /// 配置网络请求,基础地址,代理地址,如:192.168.0.123:8888(代理地址生产环境无效) + /// apiVersion 接口版本 + void _config(String baseUrl, {String proxyUrl}) { _dio = Dio(BaseOptions( method: "post", baseUrl: baseUrl, @@ -49,13 +53,13 @@ class NetUtil { _dio.interceptors.add(_NetInterceptors()); _dio.interceptors.add(LogInterceptor()); - _apiVersion = apiVersion; const bool inProduction = const bool.fromEnvironment("dart.vm.product"); if (proxyUrl != null && proxyUrl != '' && !inProduction) { _setProxy(proxyUrl); } } + /// 设置代理 void _setProxy(String proxyUrl) { (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) { @@ -67,99 +71,24 @@ class NetUtil { }; } - /// 统一添加必要的参数 - static Future> signParams( - Map params) async { - if (_apiVersion != null) { - params["version"] = _apiVersion; - } - if (Platform.isIOS) { - params["platform"] = "iOS"; - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - IosDeviceInfo iosInfo = await deviceInfo.iosInfo; - params["systemVersion"] = iosInfo.systemVersion; - } else if (Platform.isAndroid) { - params["platform"] = "Android"; - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - String androidVersion = androidInfo.version.release; - params["systemVersion"] = "Android $androidVersion"; - } - - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; //版本号 - params["appVersion"] = version; - int width = window.physicalSize.width.floor(); - int height = window.physicalSize.height.floor(); - params["resolutionRatio"] = "$width*$height"; - - String token = ''; - if (token != null && - token != '' && - (!params.containsKey('token') || params['token'] == '')) { - params['token'] = token; - } - params["time"] = getNowTime(); - - // 过滤空字段,过滤首尾空格 - Map filters = Map(); - params.forEach((key, value) { - if (key != '' && value != '') { - filters[key] = (value is String) ? value.trim() : value; - } - }); - params = filters; - - List list = List(); - params.forEach((key, value) { - list.add(key.toString() + value.toString()); - }); - params["sign"] = signWithArray(list); - return params; - } - - /// 签名 - static String signWithArray(List params) { - params.sort(); - String result = ""; - result += APPKEY; - params.forEach((param) { - result += param; - }); - result += APPKEY; - return generateMd5(result); - } - - // md5 加密 - static String generateMd5(String data) { - var content = new Utf8Encoder().convert(data); - var digest = md5.convert(content); - // 这里其实就是 digest.toString() - return hex.encode(digest.bytes); - } - - /// 获取时间戳,单位:秒 - static String getNowTime() { - int timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); - return "$timestamp"; - } - + /// 同步请求 static Future post(String path, {Map params}) async { if (params == null) { params = {}; } - // Map sign = await signParams(params); - Map sign = params; + + Map sign = await signParams(params); Response response; try { Dio dio = await NetUtil.getInstance().dio; response = await dio.post(path, data: sign); } on DioError catch (e) { _formatError(e); +// Logger.error(e); } if (response == null) { - return {}; + return null; } if (response.statusCode != 200) { @@ -169,159 +98,258 @@ class NetUtil { Fluttertoast.showToast(msg: response.statusMessage); } // log.e(response.statusMessage); - return {}; + return null; } var result = response.data; - // if (result['success'] == 0 || result['success'] == '0') { - // print('error: ' + result['msg']); - // Fluttertoast.showToast( - // msg: result['msg'], - // toastLength: Toast.LENGTH_SHORT, - // gravity: ToastGravity.BOTTOM, - // ); - // return {}; - // } - return result; - } + if (result[GlobalConfig.HTTP_RESPONSE_KEY_CODE] == + GlobalConfig.RESPONSE_SUCCESS_CODE || + result[GlobalConfig.HTTP_RESPONSE_KEY_CODE] == + '${GlobalConfig.RESPONSE_SUCCESS_CODE}') { - static Future post2(String path, - {Map params}) async { - // if (params == null) { - // params = {}; - // } - // Map sign = await signParams(params); - // Response response; - // try { - // Dio dio = await NetUtil.getInstance().dio; - // response = await dio.post(path, data: sign); - // } on DioError catch (e) { - // _formatError(e); - // } - // if (response == null) { - // return {}; - // } - - // if (response.statusCode != 200) { - // const bool inProduction = const bool.fromEnvironment("dart.vm.product"); - // if (!inProduction) { - // Fluttertoast.showToast(msg: response.statusMessage); - // } - // log.e(response.statusMessage); - // return {}; - // } - - // var result = jsonDecode(response.data); - - // if (result['success'] == 0 || result['success'] == '0') { - // print('error: ' + result['msg']); - // Fluttertoast.showToast( - // msg: result['msg'], - // toastLength: Toast.LENGTH_SHORT, - // gravity: ToastGravity.BOTTOM, - // ); - // return result; - // } - // return result['data']; - } + Logger.error(result[GlobalConfig.HTTP_RESPONSE_KEY_MSG]); - /* - * error统一处理 - */ - static void _formatError(DioError e) { - // if (e.type == DioErrorType.CONNECT_TIMEOUT) { - // log.d('连接超时: ${e.toString()}'); - // } else if (e.type == DioErrorType.SEND_TIMEOUT) { - // log.d('请求超时: ${e.toString()}'); - // } else if (e.type == DioErrorType.RECEIVE_TIMEOUT) { - // log.d('响应超时: ${e.toString()}'); - // } else if (e.type == DioErrorType.RESPONSE) { - // log.d('出现异常: ${e.toString()}'); - // } else if (e.type == DioErrorType.CANCEL) { - // log.d('请求取消: ${e.toString()}'); - // } else { - // log.d('未知错误: ${e.toString()}'); - // } + Fluttertoast.showToast( + msg: result[GlobalConfig.HTTP_RESPONSE_KEY_MSG], + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + return result; } + /// 异步请求 static void request(String path, {String method = 'POST', Map params, Map multiFiles, OnSuccess onSuccess, - OnError onError}) async { + OnError onError, + OnCache onCache}) async { if (params == null) { params = {}; } - signParams(params).then((sign) async { - print('params => ${sign.toString()}'); + + // 根据请求参数,获取缓存的Key + String cacheKey = _getRequestParamsCachedKey(params, path); +// // 读取缓存回调 + await _onCallBackCacheData(onCache, cacheKey); + // 参数签名 + Map sign = await signParams(params); + + Response response; + try { Dio dio = await NetUtil.getInstance().dio; + // 文件 if (multiFiles != null && multiFiles.length > 0) { for (String key in multiFiles.keys) { var file = await MultipartFile.fromFile(multiFiles[key].path); sign[key] = file; } } + FormData formData = FormData.fromMap(sign); + response = await dio.request(path, + data: formData, options: Options(method: method)); + } on DioError catch (e) { + _formatError(e); + } - FormData formdata = FormData.fromMap(sign); - return dio.request(path, - data: formdata, options: Options(method: method)); - }).then((response) { - var result = jsonDecode(response.data); - if (result['success'] == 0 || result['success'] == '0') { - print('error: ' + result['msg']); - Fluttertoast.showToast( - msg: result['msg'], - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - ); - if (onError != null) { - onError(result['msg'] ?? '未知错误'); - } - return; - } + if (response == null) { + return null; + } + + var result = jsonDecode(response.data); + //TODO 加密? + if (result[GlobalConfig.HTTP_RESPONSE_KEY_CODE] == + GlobalConfig.RESPONSE_SUCCESS_CODE || + result[GlobalConfig.HTTP_RESPONSE_KEY_CODE] == + '${GlobalConfig.RESPONSE_SUCCESS_CODE}') { if (onSuccess != null) { - onSuccess(result['data']); + onSuccess(result[GlobalConfig.HTTP_RESPONSE_KEY_DATA]); + // 缓存返回的数据 + _setCallBackCacheData(cacheKey, response.data); + } + return; + } + + Logger.error('error: ' + result[GlobalConfig.HTTP_RESPONSE_KEY_MSG]); + Fluttertoast.showToast( + msg: result[GlobalConfig.HTTP_RESPONSE_KEY_MSG], + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + if (onError != null) { + onError(result[GlobalConfig.HTTP_RESPONSE_KEY_MSG] ?? '未知错误'); + } + return; + } + + /// 统一添加必要的参数 + static Future> signParams( + Map params) async { + // 应用信息 + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + // 原生传的信息 + Map setting = await NativeUtil.getSetting(); + + if (Platform.isIOS) { + IosDeviceInfo iosInfo = await DeviceInfoPlugin().iosInfo; + // 设备 + params["platform"] = "ios"; + // 设备系统版本 + params["system_version"] = iosInfo?.systemVersion; + // 设备型号 如:iPhone 11pro + params['device_model'] = iosInfo?.name; + // 设备型号,如:MLMF2LL/A + // params['device_number'] = iosInfo?.model; + // 设备ID + params['device_id'] = iosInfo?.identifierForVendor; + } else if (Platform.isAndroid) { + AndroidDeviceInfo androidInfo = await DeviceInfoPlugin().androidInfo; + // 设备 + params["platform"] = "android"; + // 设备系统版本 + params["system_version"] = "Android ${androidInfo?.version?.release}"; + // 设备型号 如:iPhone 11pro + params['device_model'] = androidInfo?.model; + // 设备型号,如:MLMF2LL/A + // params['device_number'] = androidInfo?.id; + // 设备Id + params['device_id'] = androidInfo?.androidId; + } + // 应用版本号 + params["app_version"] = packageInfo.version; + // 分辨率 + params["solution"] = + "${window.physicalSize.width.floor()}*${window.physicalSize.height.floor()}"; + + // 站长ID + String masterId = setting['master_id']; + if (null != null && + masterId != '' && + (!params.containsKey('master_id') || params['master_id'] == '')) { + params['master_id'] = masterId; + } + + // secret_key + params['secret_key'] = setting['secret_key']; + + // token + String token = setting['token']; + if (token != null && + token != '' && + (!params.containsKey('token') || params['token'] == '')) { + params['token'] = token; + } + + // 当前时间戳:秒 + params["time"] = TimeUtil.getNowTime(); + + // 过滤空字段,过滤首尾空格 + Map filters = Map(); + params.forEach((key, value) { + if (key != '' && value != '') { + filters[key] = (value is String) ? value.trim() : value; } - }).catchError((error) { - onError(error.toString()); }); + params = filters; + + List list = List(); + params.forEach((key, value) { + list.add(key.toString() + value.toString()); + }); + params["sign"] = signWithArray(list); + return params; + } + + /// 获取请求缓存成功的数据 + static Future _onCallBackCacheData(OnCache onCache, String cacheKey) async { + // 读取缓存 + Map cacheMap = await SharedPreferencesUtil.getNetCacheResult(cacheKey); + if (!EmptyUtil.isEmpty(cacheMap) && + cacheMap.containsKey(GlobalConfig.RESPONSE_SUCCESS_CODE) && + (cacheMap[GlobalConfig.RESPONSE_SUCCESS_CODE] == + GlobalConfig.RESPONSE_SUCCESS_CODE || + cacheMap[GlobalConfig.RESPONSE_SUCCESS_CODE] == + '${GlobalConfig.RESPONSE_SUCCESS_CODE}') && + null != cacheMap[GlobalConfig.HTTP_RESPONSE_KEY_DATA]) { + onCache(cacheMap['data']); + return; + } + return; + } + + /// 缓存请求成功的数据 + static void _setCallBackCacheData(String cacheKey, String value) async { + SharedPreferencesUtil.setNetCacheResult(cacheKey, value); + } + + /// 根据请求参数,获取缓存的Key + static String _getRequestParamsCachedKey( + Map map, String path) { + if (EmptyUtil.isEmpty(map)) { + return EncodeUtil.generateMd5(path); + } + return EncodeUtil.generateMd5(path + map.toString()); + } + + /// 签名 + static String signWithArray(List params) { + // 字母升序 + params.sort(); + String result = ""; +// result += secret_key; + params.forEach((param) { + result += param; + }); +// result += secret_key; + return EncodeUtil.generateMd5(result); + } + + /* + * error统一处理 + */ + static void _formatError(DioError e) { + if (e.type == DioErrorType.CONNECT_TIMEOUT) { + Logger.error('连接超时: ${e.toString()}'); + } else if (e.type == DioErrorType.SEND_TIMEOUT) { + Logger.error('请求超时: ${e.toString()}'); + } else if (e.type == DioErrorType.RECEIVE_TIMEOUT) { + Logger.error('响应超时: ${e.toString()}'); + } else if (e.type == DioErrorType.RESPONSE) { + Logger.error('出现异常: ${e.toString()}'); + } else if (e.type == DioErrorType.CANCEL) { + Logger.error('请求取消: ${e.toString()}'); + } else { + Logger.error('未知错误: ${e.toString()}'); + } } } /** * @description: 网络请求拦截器 - * @param {type} - * @return: + * @param {type} + * @return: */ class _NetInterceptors extends InterceptorsWrapper { @override Future onRequest(RequestOptions options) { - print("${options?.method} => ${options?.path}"); - + Logger.net(options?.path, data: options.data.toString()); + // TODO 加密? return super.onRequest(options); } @override Future onResponse(Response response) { - print("response[${response?.statusCode}] => ${response?.data}"); - + Logger.endNet(response.realUri.toString(), data: response?.data?.toString() ?? ''); + // TODO 解密? return super.onResponse(response); - // String data = response?.data; - // Map map = json.decode(data); - // var success = map['success']; - // if (success != null && '$success' == '1') { - // return super.onResponse(response); - // } else { - // response.data = response?.data['msg'] ?? '未知错误'; - // return NetUtil._dio.reject(Response(data: response)); - // } } @override Future onError(DioError err) { - print("ERROR[${err?.response?.statusCode}] => PATH: ${err?.request?.path}"); +// Logger.error(err); return super.onError(err); } } diff --git a/lib/util/shared_prefe_util.dart b/lib/util/shared_prefe_util.dart new file mode 100644 index 0000000..ef876e6 --- /dev/null +++ b/lib/util/shared_prefe_util.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:zhiying_comm/util/empty_util.dart'; + +class SharedPreferencesUtil { + static Future> getNetCacheResult(String key) async { + if (!EmptyUtil.isEmpty(key)) { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String cacheResult = prefs.getString(key); + // TODO need解密? + if (!EmptyUtil.isEmpty(cacheResult)) { + Map map = json.decode(cacheResult); + if(!EmptyUtil.isEmpty(map)) { + return map; + } + } + } + return null; + } + + static Future setNetCacheResult(String key, String value) async { + if (!EmptyUtil.isEmpty(key) && !EmptyUtil.isEmpty(value)) { + SharedPreferences prefs = await SharedPreferences.getInstance(); + // TODO need加密? + prefs.setString(key, value); + } + return; + } +} diff --git a/lib/util/time_util.dart b/lib/util/time_util.dart new file mode 100644 index 0000000..c0eb6db --- /dev/null +++ b/lib/util/time_util.dart @@ -0,0 +1,10 @@ + +class TimeUtil{ + + /// 获取时间戳,单位:秒 + static String getNowTime() { + int timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); + return "$timestamp"; + } + +} \ No newline at end of file diff --git a/lib/zhiying_comm.dart b/lib/zhiying_comm.dart index a7bb149..03feb4f 100644 --- a/lib/zhiying_comm.dart +++ b/lib/zhiying_comm.dart @@ -13,6 +13,7 @@ export 'util/net_util.dart'; export 'util/page_factory.dart'; export 'util/widget_factory.dart'; export 'util/router_util.dart'; +export 'util/log/let_log.dart'; export 'util/enum_util.dart'; export 'util/encode_util.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 3a4c091..212e2c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,11 @@ dependencies: flutter_native_image: ^0.0.5 fluttertoast: ^7.0.4 cached_network_image: ^2.2.0+1 + # 屏幕适配 + flutter_screenutil: ^1.1.0 + # 缓存 + shared_preferences: ^0.5.10 + dev_dependencies: flutter_test: