Template
chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库 7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系 5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter 通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sunny_mochi/core/error/exception_mapper.dart';
|
||||
import 'package:sunny_mochi/core/widgets/app_toast.dart';
|
||||
import 'package:sunny_mochi/features/error_report/data/error_logger.dart';
|
||||
|
||||
extension BuildContextX on BuildContext {
|
||||
/// 弹一个 Toast 风格的 SnackBar(对应 iOS Mkt.makeToast(...))。
|
||||
void showToast(
|
||||
String message, {
|
||||
ToastType type = ToastType.info,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) => AppToast.show(this, message, type: type, duration: duration);
|
||||
|
||||
/// 异常 → ExceptionMapper.fromUnknown(e, st) → toast(Failure.message)。
|
||||
///
|
||||
/// 兜底替代散落的 `on Object catch (e, st) { mapper.fromUnknown; if(mounted) showToast }`
|
||||
/// 模板(5+ 处 mine 表单提交流程)。
|
||||
void showError(WidgetRef ref, Object error, StackTrace stackTrace) {
|
||||
final failure =
|
||||
ref.read(exceptionMapperProvider).fromUnknown(error, stackTrace);
|
||||
// 写入本地错误日志(fire-and-forget,不阻塞 UI)
|
||||
unawaited(ref.read(errorLoggerProvider).log(failure));
|
||||
if (mounted) showToast(failure.userMessage, type: ToastType.error);
|
||||
}
|
||||
|
||||
/// 当前 Theme 快捷读取。
|
||||
ThemeData get theme => Theme.of(this);
|
||||
ColorScheme get colors => theme.colorScheme;
|
||||
TextTheme get textTheme => theme.textTheme;
|
||||
|
||||
/// 屏幕信息快捷读取。
|
||||
Size get screenSize => MediaQuery.sizeOf(this);
|
||||
EdgeInsets get screenPadding => MediaQuery.paddingOf(this);
|
||||
double get screenWidth => screenSize.width;
|
||||
double get screenHeight => screenSize.height;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
extension DateTimeX on DateTime {
|
||||
/// yyyy-MM-dd
|
||||
String get fmtDate => DateFormat('yyyy-MM-dd').format(this);
|
||||
|
||||
/// yyyy-MM-dd HH:mm:ss
|
||||
String get fmtDateTime => DateFormat('yyyy-MM-dd HH:mm:ss').format(this);
|
||||
|
||||
/// HH:mm
|
||||
String get fmtHm => DateFormat('HH:mm').format(this);
|
||||
|
||||
/// 友好相对时间:"刚刚 / 5 分钟前 / 昨天 18:24 / 03-15 / 2024-03-15"
|
||||
String get toFriendly {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(this);
|
||||
|
||||
if (diff.inSeconds < 60) return '刚刚';
|
||||
if (diff.inMinutes < 60) return '${diff.inMinutes} 分钟前';
|
||||
if (diff.inHours < 24 && now.day == day) {
|
||||
return '今天 $fmtHm';
|
||||
}
|
||||
if (diff.inDays < 2 && now.day - day == 1) {
|
||||
return '昨天 $fmtHm';
|
||||
}
|
||||
if (year == now.year) {
|
||||
return DateFormat('MM-dd HH:mm').format(this);
|
||||
}
|
||||
return fmtDate;
|
||||
}
|
||||
}
|
||||
|
||||
extension NullableDateTimeX on DateTime? {
|
||||
String get orPlaceholder => this?.fmtDateTime ?? '-';
|
||||
String get orPlaceholderDate => this?.fmtDate ?? '-';
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
extension NumX on num {
|
||||
/// 金额格式化:1234.5 → "1,234.50"
|
||||
String get yuan =>
|
||||
'¥${toStringAsFixed(2).replaceAllMapped(
|
||||
RegExp(r'(\d)(?=(\d{3})+\.)'),
|
||||
(m) => '${m[1]},',
|
||||
)}';
|
||||
|
||||
/// 百分比:0.75 → "75%"
|
||||
String get percent => '${(this * 100).toStringAsFixed(0)}%';
|
||||
|
||||
/// 文件大小:1024 → "1 KB"
|
||||
String get bytesFmt {
|
||||
if (this < 1024) return '$this B';
|
||||
if (this < 1024 * 1024) return '${(this / 1024).toStringAsFixed(1)} KB';
|
||||
if (this < 1024 * 1024 * 1024) {
|
||||
return '${(this / 1024 / 1024).toStringAsFixed(1)} MB';
|
||||
}
|
||||
return '${(this / 1024 / 1024 / 1024).toStringAsFixed(1)} GB';
|
||||
}
|
||||
}
|
||||
|
||||
extension NullableNumX on num? {
|
||||
String get orPlaceholder => this?.toString() ?? '-';
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/// 对应 iOS BasicModule/Extension/Extension+Optional.swift。
|
||||
extension NullableStringX on String? {
|
||||
/// null 或空字符串时返回 "-"(对应 iOS orPlaceholder)。
|
||||
/// UI 层硬约束:所有 Optional 字符串展示前必须 .orPlaceholder。
|
||||
String get orPlaceholder {
|
||||
if (this == null || this!.isEmpty) return '-';
|
||||
return this!;
|
||||
}
|
||||
|
||||
/// 简洁的非空判断(替代散落的 `s != null && s!.isNotEmpty` 三段式)。
|
||||
bool get isNotNullOrEmpty => this != null && this!.isNotEmpty;
|
||||
|
||||
/// 反向:null 或空。
|
||||
bool get isNullOrEmpty => this == null || this!.isEmpty;
|
||||
}
|
||||
|
||||
extension NullableIntX on int? {
|
||||
String get orPlaceholder => this?.toString() ?? '-';
|
||||
}
|
||||
|
||||
extension NullableDoubleX on double? {
|
||||
String get orPlaceholder => this?.toString() ?? '-';
|
||||
}
|
||||
|
||||
extension StringX on String {
|
||||
/// 首字母大写。
|
||||
String get capitalize =>
|
||||
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
|
||||
|
||||
/// 中国大陆手机号校验(对应 iOS Validator.isValidPhone)。
|
||||
bool get isMobile => RegExp(r'^1[3-9]\d{9}$').hasMatch(this);
|
||||
|
||||
/// 身份证号校验(对应 iOS Validator.isValidIDCard)。
|
||||
bool get isIdCard => RegExp(r'^\d{17}[\dXx]$').hasMatch(this);
|
||||
|
||||
/// Email 简易校验。
|
||||
bool get isEmail => RegExp(r'^[\w.+-]+@([\w-]+\.)+[\w-]{2,}$').hasMatch(this);
|
||||
|
||||
/// 脱敏手机号(保留首 3 + 末 4,中间 4 个 *):13800138000 → 138****8000
|
||||
String get masked {
|
||||
if (length < 7) return this;
|
||||
return '${substring(0, 3)}****${substring(length - 4)}';
|
||||
}
|
||||
|
||||
/// 密码强度(0=无 / 1=弱 / 2=中 / 3=强)。
|
||||
/// 规则:长度 ≥6 +1;含大小写字母 +1;含数字 +1;含特殊符号 +1(最高 3)。
|
||||
int get passwordStrength {
|
||||
if (length < 6) return 0;
|
||||
var score = 1;
|
||||
if (RegExp('[A-Z]').hasMatch(this) && RegExp('[a-z]').hasMatch(this)) {
|
||||
score++;
|
||||
}
|
||||
if (RegExp(r'\d').hasMatch(this)) score++;
|
||||
if (RegExp(r'[!@#$%^&*(),.?":{}|<>_+\-=\[\]/\\;~`]').hasMatch(this)) {
|
||||
score++;
|
||||
}
|
||||
return score.clamp(0, 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user