本次提交包含多项核心更新: 1. 全量替换项目内所有xianyan.app域名变更为s2ss.com,包含配置文件、路由、隐私政策等 2. 重构图表库从fl_chart迁移至syncfusion_flutter_charts,优化图表渲染效果 3. 新增宽屏分屏布局支持,包含右侧面板注册表与可拖拽分割线 4. 完善触觉反馈服务与认证感知Mixin,修复多处内存泄漏问题 5. 合并勋章墙与金币记录入口至成就中心,简化个人中心导航 6. 新增收藏与时间线数据合并导入功能 7. 修复多处UI样式问题,统一主题颜色使用规范 8. 新增日历同步与跨平台触觉反馈依赖库 9. 修复BotToast初始化流程,避免路由切换时的弹窗崩溃
199 lines
5.4 KiB
Dart
199 lines
5.4 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — Toast提示通用封装
|
||
/// 创建时间: 2026-04-28
|
||
/// 更新时间: 2026-05-29
|
||
/// 作用: 基于 bot_toast 封装统一的Toast提示组件
|
||
/// 上次更新: 新增_isInitialized安全守卫,防止BotToast未初始化时崩溃
|
||
///
|
||
/// BotToast 初始化顺序(必须在App启动时严格遵守):
|
||
/// 1. MaterialApp.builder 中调用 BotToastInit() 创建 builder
|
||
/// 2. 立即调用 AppToast.markInitialized() 标记初始化完成
|
||
/// 3. MaterialApp.navigatorObservers 中添加 BotToastNavigatorObserver()
|
||
/// 4. GoRouter.observers 中添加 BotToastNavigatorObserver()
|
||
///
|
||
/// ⚠️ 任何在初始化完成前调用 AppToast 方法的行为将被降级为 debugPrint
|
||
/// ⚠️ BotToastNavigatorObserver 缺失会导致路由切换时 Toast 状态异常
|
||
/// ============================================================
|
||
|
||
import 'package:bot_toast/bot_toast.dart';
|
||
import 'package:flutter/cupertino.dart';
|
||
|
||
import '../../../core/theme/app_theme.dart';
|
||
import '../../../core/theme/app_spacing.dart';
|
||
import '../../../core/theme/app_typography.dart';
|
||
|
||
class AppToast {
|
||
AppToast._();
|
||
|
||
static bool _isInitialized = false;
|
||
|
||
static bool get isInitialized => _isInitialized;
|
||
|
||
static void markInitialized() {
|
||
_isInitialized = true;
|
||
debugPrint('[AppToast] BotToast 已标记为初始化完成');
|
||
}
|
||
|
||
static void markDisposed() {
|
||
_isInitialized = false;
|
||
}
|
||
|
||
static void _safeCall(VoidCallback action, {String? fallbackMessage}) {
|
||
if (_isInitialized) {
|
||
try {
|
||
action();
|
||
} catch (e) {
|
||
debugPrint('[AppToast] BotToast调用异常: $e');
|
||
}
|
||
} else {
|
||
debugPrint(
|
||
'[AppToast] BotToast未初始化,跳过Toast: ${fallbackMessage ?? '未知消息'}',
|
||
);
|
||
}
|
||
}
|
||
|
||
static void show(
|
||
String message, {
|
||
Duration duration = const Duration(seconds: 2),
|
||
bool showClose = false,
|
||
}) {
|
||
_safeCall(
|
||
() => BotToast.showCustomText(
|
||
duration: duration,
|
||
onlyOne: true,
|
||
clickClose: true,
|
||
crossPage: true,
|
||
backgroundColor: const Color(0x00000000),
|
||
toastBuilder: (_) =>
|
||
_ToastWidget(message: message, showClose: showClose),
|
||
),
|
||
fallbackMessage: message,
|
||
);
|
||
}
|
||
|
||
static void showSuccess(String message) {
|
||
show('✅ $message');
|
||
}
|
||
|
||
static void showError(String message) {
|
||
show('❌ $message', duration: const Duration(seconds: 3));
|
||
}
|
||
|
||
static void showWarning(String message) {
|
||
show('⚠️ $message');
|
||
}
|
||
|
||
static void showInfo(String message) {
|
||
show('ℹ️ $message');
|
||
}
|
||
|
||
static void showLoading({String? message}) {
|
||
_safeCall(
|
||
() => BotToast.showCustomLoading(
|
||
toastBuilder: (_) => _LoadingWidget(message: message),
|
||
clickClose: false,
|
||
allowClick: false,
|
||
),
|
||
fallbackMessage: message ?? 'Loading',
|
||
);
|
||
}
|
||
|
||
static void closeLoading() {
|
||
_safeCall(BotToast.closeAllLoading, fallbackMessage: 'closeLoading');
|
||
}
|
||
}
|
||
|
||
class _ToastWidget extends StatelessWidget {
|
||
const _ToastWidget({required this.message, this.showClose = false});
|
||
|
||
final String message;
|
||
final bool showClose;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = AppTheme.ext(context);
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm + 4,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgSecondary.withValues(alpha: 0.95),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: ext.textHint.withValues(alpha: 0.1),
|
||
width: 0.5,
|
||
),
|
||
boxShadow: const [
|
||
BoxShadow(
|
||
color: Color(0x1A000000),
|
||
blurRadius: 20,
|
||
offset: Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Flexible(
|
||
child: Text(
|
||
message,
|
||
style: AppTypography.subhead.copyWith(color: ext.textPrimary),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
if (showClose) ...[
|
||
const SizedBox(width: AppSpacing.sm),
|
||
GestureDetector(
|
||
onTap: () => BotToast.cleanAll(),
|
||
child: Icon(
|
||
CupertinoIcons.xmark_circle_fill,
|
||
size: 18,
|
||
color: ext.textHint,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LoadingWidget extends StatelessWidget {
|
||
const _LoadingWidget({this.message});
|
||
|
||
final String? message;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = AppTheme.ext(context);
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgSecondary.withValues(alpha: 0.95),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: ext.textHint.withValues(alpha: 0.1),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const CupertinoActivityIndicator(radius: 14),
|
||
if (message != null) ...[
|
||
const SizedBox(height: AppSpacing.md),
|
||
Text(
|
||
message!,
|
||
style: AppTypography.subhead.copyWith(color: ext.textPrimary),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|