QinGang_interested/lib/tools/tools.dart

724 lines
21 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: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<T?> pushPage<T>(Widget page, BuildContext context) {
return Navigator.push<T>(
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<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',
);
}
}
/// ------------------------------------------------------
/// 日期格式化
/// ------------------------------------------------------
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<String, dynamic> findMapForKeyValue(
List list,
String key,
dynamic value,
) {
// 保留原有行为null 或 空字符串 返回空 Map
if (value == null) return <String, dynamic>{};
if (value is String && value.isEmpty) return <String, dynamic>{};
for (final item in list) {
if (item is! Map) continue;
final v = item[key];
if (v == null) continue;
// 1) 直接相等(类型相同或可直接比较)
if (v == value) {
return Map<String, dynamic>.from(item);
}
// 2) 数字与字符串的交叉比较("123" <-> 123
if (v is num && value is String) {
final parsed = num.tryParse(value);
if (parsed != null && parsed == v) {
return Map<String, dynamic>.from(item);
}
} else if (v is String && (value is num)) {
final parsed = num.tryParse(v);
if (parsed != null && parsed == value) {
return Map<String, dynamic>.from(item);
}
}
// 3) 最后回退到字符串比较(保险)
if (v.toString() == value.toString()) {
return Map<String, dynamic>.from(item);
}
}
return <String, dynamic>{};
}
}
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<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);
}
}
// 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<int> 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 hex108 bytes很可能就是缺 0x04 的情况
if (hex.length == 216) {
return '04' + hex; // 返回 218 hex109 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<Map<String, String>> 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": "基诺族"}
];