init
This commit is contained in:
34
lib/core/utils/date_time_formatter.dart
Normal file
34
lib/core/utils/date_time_formatter.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateTimeFormatter {
|
||||
DateTimeFormatter._();
|
||||
|
||||
static String format(
|
||||
DateTime? value, {
|
||||
String pattern = 'yyyy-MM-dd HH:mm',
|
||||
String locale = 'zh_CN',
|
||||
}) {
|
||||
if (value == null) return '';
|
||||
return DateFormat(pattern, locale).format(value);
|
||||
}
|
||||
|
||||
static String dayOrDate(String? isoString, {DateTime? now}) {
|
||||
if (isoString == null || isoString.trim().isEmpty) return '';
|
||||
|
||||
final parsed = DateTime.tryParse(isoString)?.toLocal();
|
||||
if (parsed == null) return '';
|
||||
|
||||
final current = (now ?? DateTime.now()).toLocal();
|
||||
final currentDate = DateTime(current.year, current.month, current.day);
|
||||
final parsedDate = DateTime(parsed.year, parsed.month, parsed.day);
|
||||
final diffDays = currentDate.difference(parsedDate).inDays;
|
||||
|
||||
final time = DateFormat('HH:mm').format(parsed);
|
||||
return switch (diffDays) {
|
||||
0 => '今天 $time',
|
||||
1 => '昨天 $time',
|
||||
2 => '前天 $time',
|
||||
_ => DateFormat('yyyy-MM-dd').format(parsed),
|
||||
};
|
||||
}
|
||||
}
|
||||
54
lib/core/utils/device_utils.dart
Normal file
54
lib/core/utils/device_utils.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DeviceUtils {
|
||||
DeviceUtils._();
|
||||
|
||||
static double screenWidth(BuildContext context) =>
|
||||
MediaQuery.sizeOf(context).width;
|
||||
|
||||
static double screenHeight(BuildContext context) =>
|
||||
MediaQuery.sizeOf(context).height;
|
||||
|
||||
static double topSafePadding(BuildContext context) =>
|
||||
MediaQuery.paddingOf(context).top;
|
||||
|
||||
static double bottomSafePadding(BuildContext context) =>
|
||||
MediaQuery.paddingOf(context).bottom;
|
||||
|
||||
static Future<bool> isPhysicalDevice() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
return (await plugin.androidInfo).isPhysicalDevice;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
return (await plugin.iosInfo).isPhysicalDevice;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Future<Map<String, String>> deviceInfo() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
final info = await plugin.androidInfo;
|
||||
return {
|
||||
'platform': 'android',
|
||||
'brand': info.brand,
|
||||
'model': info.model,
|
||||
'systemVersion': info.version.release,
|
||||
};
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
final info = await plugin.iosInfo;
|
||||
return {
|
||||
'platform': 'ios',
|
||||
'brand': info.systemName,
|
||||
'model': info.utsname.machine,
|
||||
'systemVersion': info.systemVersion,
|
||||
};
|
||||
}
|
||||
return {'platform': Platform.operatingSystem};
|
||||
}
|
||||
}
|
||||
25
lib/core/utils/form_validators.dart
Normal file
25
lib/core/utils/form_validators.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
class FormValidators {
|
||||
FormValidators._();
|
||||
|
||||
static String? required(String? value, {String message = '请输入内容'}) {
|
||||
if (value == null || value.trim().isEmpty) return message;
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? email(String? value, {String message = '请输入有效邮箱'}) {
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final regex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
|
||||
return regex.hasMatch(value.trim()) ? null : message;
|
||||
}
|
||||
|
||||
static String? phoneCN(String? value, {String message = '请输入有效手机号'}) {
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final regex = RegExp(r'^1[3-9]\d{9}$');
|
||||
return regex.hasMatch(value.trim()) ? null : message;
|
||||
}
|
||||
|
||||
static String? minLength(String? value, int min, {String? message}) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return value.length >= min ? null : message ?? '至少输入 $min 个字符';
|
||||
}
|
||||
}
|
||||
166
lib/core/utils/rate_limiter.dart
Normal file
166
lib/core/utils/rate_limiter.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
import 'dart:async';
|
||||
|
||||
enum RateLimitMode { debounce, throttle }
|
||||
|
||||
class ThrottleOptions {
|
||||
const ThrottleOptions({this.leading = true, this.trailing = true});
|
||||
|
||||
final bool leading;
|
||||
final bool trailing;
|
||||
}
|
||||
|
||||
class DebounceThrottle<T> {
|
||||
DebounceThrottle({
|
||||
required this.duration,
|
||||
required this.mode,
|
||||
this.onCallback,
|
||||
this.throttleOptions = const ThrottleOptions(),
|
||||
});
|
||||
|
||||
final Duration duration;
|
||||
final RateLimitMode mode;
|
||||
final void Function(T value)? onCallback;
|
||||
final ThrottleOptions throttleOptions;
|
||||
|
||||
Timer? _timer;
|
||||
T? _lastValue;
|
||||
DateTime? _lastExecuteTime;
|
||||
bool _waitingTrailing = false;
|
||||
|
||||
void call(T value) {
|
||||
_lastValue = value;
|
||||
switch (mode) {
|
||||
case RateLimitMode.debounce:
|
||||
_debounce(value);
|
||||
case RateLimitMode.throttle:
|
||||
_throttle(value);
|
||||
}
|
||||
}
|
||||
|
||||
void _debounce(T value) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(duration, () {
|
||||
onCallback?.call(value);
|
||||
_lastExecuteTime = DateTime.now();
|
||||
});
|
||||
}
|
||||
|
||||
void _throttle(T value) {
|
||||
final now = DateTime.now();
|
||||
final last = _lastExecuteTime;
|
||||
final inWindow = last != null && now.difference(last) < duration;
|
||||
|
||||
if (!inWindow && throttleOptions.leading) {
|
||||
onCallback?.call(value);
|
||||
_lastExecuteTime = now;
|
||||
_waitingTrailing = false;
|
||||
_timer?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!throttleOptions.trailing || _waitingTrailing) return;
|
||||
|
||||
_waitingTrailing = true;
|
||||
final remaining = last == null ? duration : duration - now.difference(last);
|
||||
_timer?.cancel();
|
||||
_timer = Timer(remaining, () {
|
||||
final value = _lastValue;
|
||||
if (value != null) onCallback?.call(value);
|
||||
_lastExecuteTime = DateTime.now();
|
||||
_waitingTrailing = false;
|
||||
});
|
||||
}
|
||||
|
||||
void flush() {
|
||||
_timer?.cancel();
|
||||
final value = _lastValue;
|
||||
if (value != null) {
|
||||
onCallback?.call(value);
|
||||
_lastExecuteTime = DateTime.now();
|
||||
}
|
||||
_lastValue = null;
|
||||
_waitingTrailing = false;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_lastValue = null;
|
||||
_waitingTrailing = false;
|
||||
}
|
||||
|
||||
void dispose() => cancel();
|
||||
}
|
||||
|
||||
class RateLimitHub {
|
||||
RateLimitHub({this.removeDebouncerOnDone = true});
|
||||
|
||||
final bool removeDebouncerOnDone;
|
||||
final Map<Object, DebounceThrottle<dynamic>> _debouncers = {};
|
||||
final Map<Object, DebounceThrottle<dynamic>> _throttlers = {};
|
||||
|
||||
void debounce<T>({
|
||||
required Object key,
|
||||
required T value,
|
||||
Duration duration = const Duration(milliseconds: 300),
|
||||
required void Function(T value) onCallback,
|
||||
}) {
|
||||
_debouncers[key]?.cancel();
|
||||
|
||||
late DebounceThrottle<T> limiter;
|
||||
limiter = DebounceThrottle<T>(
|
||||
duration: duration,
|
||||
mode: RateLimitMode.debounce,
|
||||
onCallback: (value) {
|
||||
onCallback(value);
|
||||
if (removeDebouncerOnDone) _debouncers.remove(key);
|
||||
},
|
||||
);
|
||||
|
||||
_debouncers[key] = limiter;
|
||||
limiter(value);
|
||||
}
|
||||
|
||||
void throttle<T>({
|
||||
required Object key,
|
||||
required T value,
|
||||
Duration duration = const Duration(milliseconds: 300),
|
||||
ThrottleOptions options = const ThrottleOptions(),
|
||||
required void Function(T value) onCallback,
|
||||
}) {
|
||||
_throttlers.putIfAbsent(
|
||||
key,
|
||||
() =>
|
||||
DebounceThrottle<T>(
|
||||
duration: duration,
|
||||
mode: RateLimitMode.throttle,
|
||||
throttleOptions: options,
|
||||
onCallback: onCallback,
|
||||
)
|
||||
as DebounceThrottle<dynamic>,
|
||||
);
|
||||
|
||||
(_throttlers[key] as DebounceThrottle<T>)(value);
|
||||
}
|
||||
|
||||
void cancel(Object key) {
|
||||
_debouncers.remove(key)?.cancel();
|
||||
_throttlers.remove(key)?.cancel();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
for (final limiter in _debouncers.values) {
|
||||
limiter.dispose();
|
||||
}
|
||||
for (final limiter in _throttlers.values) {
|
||||
limiter.dispose();
|
||||
}
|
||||
_debouncers.clear();
|
||||
_throttlers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class RateLimit {
|
||||
RateLimit._();
|
||||
|
||||
static final instance = RateLimitHub();
|
||||
}
|
||||
19
lib/core/utils/url_utils.dart
Normal file
19
lib/core/utils/url_utils.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
class UrlUtils {
|
||||
UrlUtils._();
|
||||
|
||||
static String buildQueryString(Map<String, dynamic>? params) {
|
||||
if (params == null || params.isEmpty) return '';
|
||||
|
||||
final queryParams = <String, String>{};
|
||||
for (final entry in params.entries) {
|
||||
final value = entry.value;
|
||||
if (value == null) continue;
|
||||
if (value is String || value is num || value is bool) {
|
||||
queryParams[entry.key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParams.isEmpty) return '';
|
||||
return '?${Uri(queryParameters: queryParams).query}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user