flutter_integrated_whb/lib/tools/tools.dart

636 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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';
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;
String? loginPhone;
String? loginPass;
/// 如果以下任何一项为空,则跳转到登录页
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;
void setSavePhone(String phone) => loginPhone = phone;
void setSavePass(String pass) => loginPass = pass;
}
/// ------------------------------------------------------
/// 日期格式化
/// ------------------------------------------------------
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;
/// 显示加载框(带超时,默认 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<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中根据一个 keyvalue找到对应的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;
}
}
}
class CameraPermissionHelper {
static final ImagePicker _picker = ImagePicker();
// 检查并请求相机权限(使用 ImagePicker 触发权限请求)
static Future<bool> 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<bool> 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<bool> 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<void> 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);
}
}