import 'dart:convert'; import 'dart:math'; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter/services.dart'; import 'dart:io'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:crypto/crypto.dart' as crypto; int getRandomWithNum(int min, int max) { if (max < min) { // 保护性处理:交换或抛错,这里交换 final tmp = min; min = max; max = tmp; } final random = Random(); return random.nextInt(max - min + 1) + min; // 生成 [min, max] 的随机数 } double screenHeight(BuildContext context) { double screenHeight = MediaQuery.of(context).size.height; return screenHeight; } double screenWidth(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; return screenWidth; } Future pushPage(Widget page, BuildContext context) { return Navigator.push( context, MaterialPageRoute(builder: (_) => page), ); } void presentOpaque(Widget page, BuildContext context) { Navigator.of(context).push( PageRouteBuilder( opaque: false, // 允许下层透出 barrierColor: Colors.black.withOpacity(0.5), //路由遮罩色 pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ), ); } class FocusHelper { static final FocusNode _emptyNode = FocusNode(); /// 延迟一帧后再移交焦点,避免不生效的问题 static void clearFocus(BuildContext context) { try{ WidgetsBinding.instance.addPostFrameCallback((_) async { FocusScope.of(context).requestFocus(_emptyNode); await SystemChannels.textInput.invokeMethod('TextInput.hide'); }); }catch(w) { } } } /// 文本样式工具类,返回 Text Widget class HhTextStyleUtils { /// 主要标题,返回 Text /// [text]: 文本内容 /// [color]: 文本颜色,默认黑色 /// [fontSize]: 字体大小,默认16.0 /// [bold]: 是否加粗,默认true static Text mainTitle( String text, { Color color = Colors.black, double fontSize = 14.0, bool bold = true, }) { return Text( text, style: TextStyle( color: color, fontSize: fontSize, fontWeight: bold ? FontWeight.bold : FontWeight.normal, ), ); } static TextStyle secondaryTitleStyle = TextStyle( color: Colors.black54, fontSize: 13.0, ); /// 次要标题,返回 Text /// [text]: 文本内容 /// [color]: 文本颜色,默认深灰 /// [fontSize]: 字体大小,默认14.0 /// [bold]: 是否加粗,默认false static Text secondaryTitle( String text, { Color color = Colors.black54, double fontSize = 12.0, bool bold = false, }) { return Text( text, style: TextStyle( color: color, fontSize: fontSize, fontWeight: bold ? FontWeight.bold : FontWeight.normal, ), ); } /// 小文字,返回 Text /// [text]: 文本内容 /// [color]: 文本颜色,默认灰色 /// [fontSize]: 字体大小,默认12.0 /// [bold]: 是否加粗,默认false static Text smallText( String text, { Color color = Colors.black54, double fontSize = 11.0, bool bold = false, }) { return Text( text, style: TextStyle( color: color, fontSize: fontSize, fontWeight: bold ? FontWeight.bold : FontWeight.normal, ), ); } } /// 版本信息模型类 class AppVersionInfo { final String versionName; // 版本名称(如 1.0.0) final String buildNumber; // 构建号(如 1) final String fullVersion; // 完整版本(如 1.0.0+1) AppVersionInfo({ required this.versionName, required this.buildNumber, required this.fullVersion, }); @override String toString() { return fullVersion; } } // 获取应用版本信息的方法 Future getAppVersion() async { try { final packageInfo = await PackageInfo.fromPlatform(); return AppVersionInfo( versionName: packageInfo.version, buildNumber: packageInfo.buildNumber, fullVersion: '${packageInfo.version}+${packageInfo.buildNumber}', ); } catch (e) { // 获取失败时返回默认值 return AppVersionInfo( versionName: '1.0.0', buildNumber: '1', fullVersion: '1.0.0+0', ); } } /// ------------------------------------------------------ /// 日期格式化 /// ------------------------------------------------------ String formatDate(DateTime? date, String fmt) { if (date == null) return ''; String twoDigits(int n) => n.toString().padLeft(2, '0'); final replacements = { 'yyyy': date.year.toString(), 'yy': date.year.toString().substring(2), 'MM': twoDigits(date.month), 'M': date.month.toString(), 'dd': twoDigits(date.day), 'd': date.day.toString(), 'hh': twoDigits(date.hour), 'h': date.hour.toString(), 'mm': twoDigits(date.minute), 'm': date.minute.toString(), 'ss': twoDigits(date.second), 's': date.second.toString(), }; String result = fmt; replacements.forEach((key, value) { result = result.replaceAllMapped(RegExp(key), (_) => value); }); return result; } /// 把 'yyyy-MM-dd HH:mm'(或 'yyyy-MM-ddTHH:mm')解析为 DateTime,失败返回 null DateTime? _parseYMdHm(String s) { if (s.isEmpty) return null; String t = s.trim(); // 只包含日期的情况:yyyy-MM-dd if (RegExp(r'^\d{4}-\d{2}-\d{2}$').hasMatch(t)) { return DateTime.tryParse(t); // 默认 00:00:00 } // yyyy-MM-dd HH:mm 或 yyyy-MM-ddTHH:mm if (RegExp(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}$').hasMatch(t)) { final iso = t.replaceFirst(' ', 'T') + ':00'; return DateTime.tryParse(iso); } // yyyy-MM-dd HH:mm:ss 或 yyyy-MM-ddTHH:mm:ss if (RegExp(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$').hasMatch(t)) { return DateTime.tryParse(t.replaceFirst(' ', 'T')); } // 都不匹配,返回 null return null; } /// 比较两个 'yyyy-MM-dd HH:mm' 格式字符串 /// 返回 1 (a>b), 0 (a==b), -1 (a compareYMdHmStrings(a, b) == 1; /// 便捷:a 是否 早于 b bool isBeforeStr(String a, String b) => compareYMdHmStrings(a, b) == -1; /// 判断传入时间字符串 (yyyy-MM-dd HH:mm) 是否早于当前时间 bool isBeforeNow(String timeStr) { final dt = _parseYMdHm(timeStr); if (dt == null) { throw FormatException("时间格式错误,期望 'yyyy-MM-dd HH:mm' 或 'yyyy-MM-dd HH:mm:ss'"); } return dt.isBefore(DateTime.now()); } /// ------------------------------------------------------ /// 防多次点击 /// ------------------------------------------------------ class ClickUtil { ClickUtil._(); static bool _canClick = true; /// 调用示例: /// ClickUtil.noMultipleClicks(() { /* your code */ }); static void noMultipleClicks(VoidCallback fn, {int delayMs = 2000}) { if (_canClick) { _canClick = false; fn(); Future.delayed(Duration(milliseconds: delayMs), () { _canClick = true; }); } else { debugPrint('请稍后点击'); } } } void presentPage(BuildContext context, Widget page) { Navigator.push( context, MaterialPageRoute(fullscreenDialog: true, builder: (_) => page), ); } class LoadingDialogHelper { static Timer? _timer; /// 显示加载框(带超时,默认 60 秒) static void show({String? message, Duration timeout = const Duration(seconds: 60)}) { // 先清理上一个计时器,避免重复 _timer?.cancel(); if (message != null) { EasyLoading.show(status: message); } else { EasyLoading.show(); } // 设置超时自动隐藏 _timer = Timer(timeout, () { // 保护性调用 dismiss(避免访问不存在的 isShow) try { EasyLoading.dismiss(); } catch (e) { debugPrint('EasyLoading.dismiss error: $e'); } _timer?.cancel(); _timer = null; }); } /// 隐藏加载框(手动触发) static void hide() { // 清理计时器 _timer?.cancel(); _timer = null; try { EasyLoading.dismiss(); } catch (e) { debugPrint('EasyLoading.dismiss error: $e'); } } } /// 将秒数转换为 “HH:MM:SS” 格式 String secondsCount(dynamic seconds) { // 先尝试解析出一个 double 值 double totalSeconds; if (seconds == null) { totalSeconds = 0; } else if (seconds is num) { totalSeconds = seconds.toDouble(); } else { // seconds 是字符串或其他,尝试 parse totalSeconds = double.tryParse(seconds.toString()) ?? 0.0; } // 取整秒,向下取整 final int secs = totalSeconds.floor(); final int h = (secs ~/ 3600) % 24; final int m = (secs ~/ 60) % 60; final int s = secs % 60; // padLeft 保证两位数 final String hh = h.toString().padLeft(2, '0'); final String mm = m.toString().padLeft(2, '0'); final String ss = s.toString().padLeft(2, '0'); return '$hh:$mm:$ss'; } void printLongString(String text, {int chunkSize = 800}) { final pattern = RegExp('.{1,$chunkSize}'); // 每 chunkSize 个字符一组 for (final match in pattern.allMatches(text)) { print(match.group(0)); } } /// 表单处理 class FormUtils { /// 判断 [data] 中的 [key] 是否存在“有效值”: /// - key 不存在或值为 null -> false /// - String:去掉首尾空白后非空 -> true /// - Iterable / Map:非空 -> true /// - 其它类型(int、double、bool 等)只要不为 null 就算有值 -> true static bool hasValue(Map data, String key) { if (!data.containsKey(key)) return false; final val = data[key]; if (val == null) return false; if (val is String) { return val.trim().isNotEmpty; } if (val is Iterable || val is Map) { return val.isNotEmpty; } // 数字、布尔等其它非空即可 return true; } /// 在list中根据一个 key,value,找到对应的map static Map findMapForKeyValue( List list, String key, dynamic value, ) { // 保留原有行为:null 或 空字符串 返回空 Map if (value == null) return {}; if (value is String && value.isEmpty) return {}; for (final item in list) { if (item is! Map) continue; final v = item[key]; if (v == null) continue; // 1) 直接相等(类型相同或可直接比较) if (v == value) { return Map.from(item); } // 2) 数字与字符串的交叉比较("123" <-> 123) if (v is num && value is String) { final parsed = num.tryParse(value); if (parsed != null && parsed == v) { return Map.from(item); } } else if (v is String && (value is num)) { final parsed = num.tryParse(v); if (parsed != null && parsed == value) { return Map.from(item); } } // 3) 最后回退到字符串比较(保险) if (v.toString() == value.toString()) { return Map.from(item); } } return {}; } } class NoDataWidget { static Widget show({ String text = '暂无数据', }) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset('assets/images/null.png', width: 200,), Text(text, style: TextStyle(color: Colors.grey)), ], ), ); } } class NativeOrientation { static const MethodChannel _channel = MethodChannel('app.orientation'); static Future setLandscape() async { try { final res = await _channel.invokeMethod('setOrientation', 'landscape'); return res == true; } on PlatformException catch (e) { debugPrint('PlatformException setLandscape: $e'); return false; } catch (e) { debugPrint('Unknown error setLandscape: $e'); return false; } } static Future setPortrait() async { try { final res = await _channel.invokeMethod('setOrientation', 'portrait'); return res == true; } on PlatformException catch (e) { debugPrint('PlatformException setPortrait: $e'); return false; } catch (e) { debugPrint('Unknown error setPortrait: $e'); return false; } } } class CameraPermissionHelper { static final ImagePicker _picker = ImagePicker(); // 检查并请求相机权限(使用 ImagePicker 触发权限请求) static Future checkAndRequestCameraPermission() async { if (Platform.isIOS) { // 对于 iOS,使用 ImagePicker 触发权限请求 try { // 尝试获取图片(但立即取消)来触发权限请求 final XFile? file = await _picker.pickImage( source: ImageSource.camera, maxWidth: 1, // 最小尺寸,减少处理时间 maxHeight: 1, imageQuality: 1, ).timeout(const Duration(milliseconds: 100), onTimeout: () { return null; // 超时返回 null,避免等待用户操作 }); // 无论是否成功获取文件,权限请求已经被触发 // 现在检查实际的权限状态 var status = await Permission.camera.status; return status.isGranted; } catch (e) { // 如果出现错误,回退到直接检查权限状态 var status = await Permission.camera.status; return status.isGranted; } } else { // Android 使用标准的权限检查方式 var status = await Permission.camera.status; if (status.isDenied) { status = await Permission.camera.request(); } return status.isGranted; } } // 检查并请求相册权限 static Future checkAndRequestPhotoPermission() async { if (Platform.isIOS) { // 对于 iOS,使用 ImagePicker 触发权限请求 try { // 尝试获取图片(但立即取消)来触发权限请求 final XFile? file = await _picker.pickImage( source: ImageSource.gallery, maxWidth: 1, maxHeight: 1, imageQuality: 1, ).timeout(const Duration(milliseconds: 100), onTimeout: () { return null; }); // 检查实际的权限状态 var status = await Permission.photos.status; return status.isGranted; } catch (e) { var status = await Permission.photos.status; return status.isGranted; } } else { // Android 使用标准的权限检查方式 var status = await Permission.storage.status; if (status.isDenied) { status = await Permission.storage.request(); } return status.isGranted; } } } Future checkNetworkWifi() async { final connectivityResult = await Connectivity().checkConnectivity(); if (connectivityResult == ConnectivityResult.mobile) { print("当前是移动网络(可能是 2G/3G/4G/5G)"); return false; } else if (connectivityResult == ConnectivityResult.wifi) { return true; print("当前是 WiFi"); } else if (connectivityResult == ConnectivityResult.ethernet) { print("当前是有线网络"); } else if (connectivityResult == ConnectivityResult.none) { print("当前无网络连接"); } return false; } Future openAppStore() async { String appId = '6739233192'; // 优先使用 itms-apps 直接打开 App Store 应用(iOS) final Uri uri = Uri.parse('itms-apps://itunes.apple.com/app/id$appId'); // 可选:直接打开写评论页: // final Uri uri = Uri.parse('itms-apps://itunes.apple.com/app/id$appId?action=write-review'); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); exit(0); } else { // 回退到 https 链接(在浏览器中打开 App Store 页面) final Uri webUri = Uri.parse('https://itunes.apple.com/app/id$appId'); if (await canLaunchUrl(webUri)) { await launchUrl(webUri, mode: LaunchMode.externalApplication); } else { throw 'Could not launch App Store for app id $appId'; } exit(0); } } // utils/sm2_format.dart const int C1C2C3 = 0; const int C1C3C2 = 1; bool _looksLikeHex(String s) { final str = s.trim(); return RegExp(r'^[0-9a-fA-F]+$').hasMatch(str) && str.length % 2 == 0; } /// 确保 SM2 密文的 C1 部分以 "04" 开头(hex 表示)。 /// - cipherHex: SM2.encrypt 的返回值(hex 字符串) /// - 返回:如果输入看起来不是 hex,会直接返回原值;否则返回已处理的 hex 字符串。 String ensureC1Has04(String cipherHex) { if (cipherHex == null) return cipherHex; String s = cipherHex.trim(); if (!_looksLikeHex(s)) return s; if (s.startsWith('04')) return s; // 最小判断:如果长度足够(至少有 128(hex) 的 C1 + 64(hex) 的 C3) // 128 hex = 64 bytes (X+Y each 32 bytes), C3 = 32 bytes = 64 hex if (s.length >= 128 + 64) { // 直接在开头插 '04',把 c1 从 128->130 hex return '04' + s; } // 长度不符合常见 SM2 (可能是 base64 / 其他格式),不做改动 return s; } /// 去掉开头的 04(如果后端需要无 04 的 c1) String removeC1Prefix04(String cipherHex) { if (cipherHex == null) return cipherHex; String s = cipherHex.trim(); if (!_looksLikeHex(s)) return s; if (s.startsWith('04') && s.length >= 130) { return s.substring(2); } return s; } /// 尝试把输入视作 hex,并返回 hex 字符(或原样返回) String normalizeToHexIfPossible(dynamic input) { if (input == null) return ''; if (input is String) { final s = input.trim(); if (_looksLikeHex(s)) return s; // 如果是 base64,尝试 decode -> hex try { final bytes = base64.decode(s); return bytesToHex(bytes); } catch (e) { return s; } } return input.toString(); } /// 辅助:bytes -> hex String bytesToHex(List bytes) { final sb = StringBuffer(); for (final b in bytes) { sb.write(b.toRadixString(16).padLeft(2, '0')); } return sb.toString(); } String normalizeSm2Hex(String hex) { hex = hex.replaceAll(RegExp(r'\s+'), '').toLowerCase(); // 如果长度是 216 hex(108 bytes),很可能就是缺 0x04 的情况 if (hex.length == 216) { return '04' + hex; // 返回 218 hex(109 bytes) } // 否则按原样返回(也可加更多校验) return hex; } String md5Hex(String input) { final bytes = utf8.encode(input); final digest = crypto.md5.convert(bytes); return digest.bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } /// 56个民族数据字典 List> nationMapList = [ {"code": "01", "name": "汉族"}, {"code": "02", "name": "蒙古族"}, {"code": "03", "name": "回族"}, {"code": "04", "name": "藏族"}, {"code": "05", "name": "维吾尔族"}, {"code": "06", "name": "苗族"}, {"code": "07", "name": "彝族"}, {"code": "08", "name": "壮族"}, {"code": "09", "name": "布依族"}, {"code": "10", "name": "朝鲜族"}, {"code": "11", "name": "满族"}, {"code": "12", "name": "侗族"}, {"code": "13", "name": "瑶族"}, {"code": "14", "name": "白族"}, {"code": "15", "name": "土家族"}, {"code": "16", "name": "哈尼族"}, {"code": "17", "name": "哈萨克族"}, {"code": "18", "name": "傣族"}, {"code": "19", "name": "黎族"}, {"code": "20", "name": "傈僳族"}, {"code": "21", "name": "佤族"}, {"code": "22", "name": "畲族"}, {"code": "23", "name": "高山族"}, {"code": "24", "name": "拉祜族"}, {"code": "25", "name": "水族"}, {"code": "26", "name": "东乡族"}, {"code": "27", "name": "纳西族"}, {"code": "28", "name": "景颇族"}, {"code": "29", "name": "柯尔克孜族"}, {"code": "30", "name": "土族"}, {"code": "31", "name": "达斡尔族"}, {"code": "32", "name": "仫佬族"}, {"code": "33", "name": "羌族"}, {"code": "34", "name": "布朗族"}, {"code": "35", "name": "撒拉族"}, {"code": "36", "name": "毛南族"}, {"code": "37", "name": "仡佬族"}, {"code": "38", "name": "锡伯族"}, {"code": "39", "name": "阿昌族"}, {"code": "40", "name": "普米族"}, {"code": "41", "name": "塔吉克族"}, {"code": "42", "name": "怒族"}, {"code": "43", "name": "乌孜别克族"}, {"code": "44", "name": "俄罗斯族"}, {"code": "45", "name": "鄂温克族"}, {"code": "46", "name": "德昂族"}, {"code": "47", "name": "保安族"}, {"code": "48", "name": "裕固族"}, {"code": "49", "name": "京族"}, {"code": "50", "name": "塔塔尔族"}, {"code": "51", "name": "独龙族"}, {"code": "52", "name": "鄂伦春族"}, {"code": "53", "name": "赫哲族"}, {"code": "54", "name": "门巴族"}, {"code": "55", "name": "珞巴族"}, {"code": "56", "name": "基诺族"} ];