516 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			516 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
| 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';
 | ||
| 
 | ||
| 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<T?> pushPage<T>(Widget page, BuildContext context) {
 | ||
|   return Navigator.push<T>(
 | ||
|     context,
 | ||
|     MaterialPageRoute(builder: (_) => page),
 | ||
|   );
 | ||
| }
 | ||
| void present(Widget page, BuildContext context) {
 | ||
|   Navigator.of(context).push(
 | ||
|     PageRouteBuilder(
 | ||
|       pageBuilder: (context, animation, secondaryAnimation) => page,
 | ||
|       transitionDuration: Duration.zero,
 | ||
|       reverseTransitionDuration: Duration.zero,
 | ||
|     ),
 | ||
|   );
 | ||
| }
 | ||
| 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<AppVersionInfo> 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',
 | ||
|     );
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// ------------------------------------------------------
 | ||
| /// 全局会话管理
 | ||
| /// ------------------------------------------------------
 | ||
| class SessionService {
 | ||
|   SessionService._();
 | ||
| 
 | ||
|   static final SessionService instance = SessionService._();
 | ||
| 
 | ||
|   String? corpinfoId;
 | ||
|   String? loginUserId;
 | ||
|   Map<String, dynamic>? loginUser;
 | ||
|   String? deptId;
 | ||
|   String? deptLevel;
 | ||
|   String? postId;
 | ||
|   String? username;
 | ||
|   String? version;
 | ||
|   String? basePath;
 | ||
|   String? isRest;
 | ||
|   List<dynamic>? permission;
 | ||
|   bool updateInfo = false;
 | ||
|   String? dangerJson;
 | ||
|   String? riskJson;
 | ||
|   String? departmentJsonStr;
 | ||
|   String? departmentHiddenTypeJsonStr;
 | ||
|   String? customRecordDangerJson;
 | ||
|   String? unqualifiedInspectionItemID;
 | ||
|   String? listItemNameJson;
 | ||
|   String? studyToken;
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|   /// 如果以下任何一项为空,则跳转到登录页
 | ||
|   void loginSession(BuildContext context) {
 | ||
|     if (corpinfoId == null || loginUserId == null || loginUser == null) {
 | ||
|       Navigator.pushReplacementNamed(context, '/login');
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // setters
 | ||
|   void setLoginUser(Map<String, dynamic> user) => loginUser = user;
 | ||
|   void setStudyToken(String token) => studyToken = token;
 | ||
| 
 | ||
|   void setLoginUserId(String id) => loginUserId = id;
 | ||
| 
 | ||
|   void setCorpinfoId(String id) => corpinfoId = id;
 | ||
| 
 | ||
|   void setDeptId(String id) => deptId = id;
 | ||
| 
 | ||
|   void setDeptLevel(String level) => deptLevel = level;
 | ||
| 
 | ||
|   void setPostId(String id) => postId = id;
 | ||
| 
 | ||
|   void setUsername(String name) => username = name;
 | ||
| 
 | ||
|   void setVersion(String ver) => version = ver;
 | ||
| 
 | ||
|   void setBasePath(String url) => basePath = url;
 | ||
| 
 | ||
|   void setIsRest(String rest) => isRest = rest;
 | ||
| 
 | ||
|   void setPermission(List<dynamic> list) => permission = list;
 | ||
| 
 | ||
|   void setUpdateInfo(bool flag) => updateInfo = flag;
 | ||
| 
 | ||
|   void setDangerWaitInfo(String json) => dangerJson = json;
 | ||
| 
 | ||
|   void setRiskWaitInfo(String json) => riskJson = json;
 | ||
| 
 | ||
|   void setDepartmentJsonStr(String json) => departmentJsonStr = json;
 | ||
| 
 | ||
|   void setCustomRecordDangerJson(String json) => customRecordDangerJson = json;
 | ||
| 
 | ||
|   void setUnqualifiedInspectionItemIDJson(String json) => unqualifiedInspectionItemID = json;
 | ||
| 
 | ||
|   void setListItemNameJson(String json) => listItemNameJson = json;
 | ||
| 
 | ||
| }
 | ||
| 
 | ||
| /// ------------------------------------------------------
 | ||
| /// 日期格式化
 | ||
| /// ------------------------------------------------------
 | ||
| String formatDate(DateTime? date, String fmt) {
 | ||
|   if (date == null) return '';
 | ||
|   String twoDigits(int n) => n.toString().padLeft(2, '0');
 | ||
| 
 | ||
|   final replacements = <String, String>{
 | ||
|     '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<b)
 | ||
| int compareYMdHmStrings(String a, String b) {
 | ||
|   final da = _parseYMdHm(a);
 | ||
|   final db = _parseYMdHm(b);
 | ||
| 
 | ||
|   if (da == null || db == null) {
 | ||
|     throw FormatException("时间格式错误,期望 'yyyy-MM-dd HH:mm' 或 'yyyy-MM-dd HH:mm:ss'");
 | ||
|   }
 | ||
| 
 | ||
|   if (da.isAtSameMomentAs(db)) return 0;
 | ||
|   return da.isAfter(db) ? 1 : -1;
 | ||
| }
 | ||
| /// 便捷:a 是否 晚于 b
 | ||
| bool isAfterStr(String a, String b) => 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;
 | ||
| 
 | ||
|   /// 显示加载框(带超时,默认 10 秒)
 | ||
|   static void show({String? message, Duration timeout = const Duration(seconds: 10)}) {
 | ||
|     // 先清理上一个计时器,避免重复
 | ||
|     _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<dynamic, dynamic> 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, String value) {
 | ||
|     Map target = list.firstWhere(
 | ||
|           (item) => item[key] == value,
 | ||
|       orElse: () => {},
 | ||
|     );
 | ||
|     return target ?? {};
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| class NoDataWidget {
 | ||
|   static Widget show() {
 | ||
|     return Center(
 | ||
|       child: Column(
 | ||
|         mainAxisAlignment: MainAxisAlignment.center,
 | ||
|         children: [
 | ||
|           Image.asset('assets/images/null.png', width: 200,),
 | ||
|           Text('暂无数据', style: TextStyle(color: Colors.grey)),
 | ||
|         ],
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
| }
 | ||
| class NativeOrientation {
 | ||
|   static const MethodChannel _channel = MethodChannel('app.orientation');
 | ||
| 
 | ||
|   static Future<bool> 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<bool> 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;
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| 
 |