loading_dialog.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import 'package:dio/dio.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_screenutil/flutter_screenutil.dart';
  4. import 'package:lottie/lottie.dart';
  5. import 'package:get/get.dart';
  6. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  7. import '../../components/ix_snackbar.dart';
  8. import '../../data/models/api_exception.dart';
  9. import '../../data/models/failure.dart';
  10. import '../../../config/translations/strings_enum.dart';
  11. import '../../widgets/submit_btn.dart';
  12. import '../error_dialog.dart';
  13. enum LoadingState { loading, success }
  14. class LoadingDialog extends StatefulWidget {
  15. final String? loadingText;
  16. final String? successText;
  17. final Future<void> Function() onRequest; // 异步请求函数
  18. final VoidCallback? onSuccess; // 成功后的回调
  19. final VoidCallback? onError; // 错误后的回调
  20. final bool canCancel; // 是否可以取消
  21. final VoidCallback? onCancel; // 取消后的回调
  22. const LoadingDialog({
  23. super.key,
  24. this.loadingText = 'Processing...',
  25. this.successText = 'Success',
  26. required this.onRequest,
  27. this.onSuccess,
  28. this.onError,
  29. this.canCancel = false, // 默认为false
  30. this.onCancel,
  31. });
  32. static Future<void> show({
  33. required BuildContext context,
  34. required Future<void> Function() onRequest,
  35. String? loadingText,
  36. String? successText,
  37. VoidCallback? onSuccess,
  38. VoidCallback? onError,
  39. bool canCancel = false,
  40. VoidCallback? onCancel,
  41. }) {
  42. return showDialog(
  43. context: context,
  44. barrierDismissible: false,
  45. builder: (context) => LoadingDialog(
  46. loadingText: loadingText ?? Strings.processing.tr,
  47. successText: successText ?? Strings.success.tr,
  48. onRequest: onRequest,
  49. onSuccess: onSuccess,
  50. onError: onError,
  51. canCancel: canCancel,
  52. onCancel: onCancel,
  53. ),
  54. );
  55. }
  56. @override
  57. State<LoadingDialog> createState() => _LoadingDialogState();
  58. }
  59. class _LoadingDialogState extends State<LoadingDialog>
  60. with TickerProviderStateMixin {
  61. LoadingState _state = LoadingState.loading;
  62. late final AnimationController _controller;
  63. final Duration _loadingDuration = const Duration(milliseconds: 3000);
  64. final Duration _successDuration = const Duration(milliseconds: 1500);
  65. bool _isCancelled = false;
  66. @override
  67. void initState() {
  68. super.initState();
  69. _controller = AnimationController(vsync: this);
  70. _handleRequest();
  71. }
  72. @override
  73. void dispose() {
  74. _controller.dispose();
  75. super.dispose();
  76. }
  77. Future<void> _handleRequest() async {
  78. try {
  79. if (_isCancelled) return;
  80. await widget.onRequest();
  81. if (_isCancelled) return;
  82. setState(() {
  83. _state = LoadingState.success;
  84. _controller.duration = _successDuration;
  85. _controller.forward();
  86. });
  87. // 成功后延迟关闭对话框
  88. await Future.delayed(_successDuration);
  89. if (mounted) {
  90. Navigator.of(context).pop();
  91. widget.onSuccess?.call();
  92. }
  93. } catch (e, s) {
  94. if (mounted) {
  95. Navigator.of(context).pop();
  96. widget.onError?.call();
  97. handleSnackBarError(e, s);
  98. }
  99. }
  100. }
  101. void handleSnackBarError(dynamic error, StackTrace stackTrace) {
  102. if (error is ApiException) {
  103. ErrorDialog.show(message: error.message);
  104. } else if (error is Failure) {
  105. ErrorDialog.show(message: error.message ?? Strings.unknownError.tr);
  106. } else if (error is DioException) {
  107. switch (error.type) {
  108. case DioExceptionType.connectionError:
  109. case DioExceptionType.connectionTimeout:
  110. case DioExceptionType.receiveTimeout:
  111. case DioExceptionType.sendTimeout:
  112. IXSnackBar.showIXErrorSnackBar(
  113. title: Strings.error.tr,
  114. message: Strings.unableToConnectNetwork.tr,
  115. );
  116. break;
  117. default:
  118. IXSnackBar.showIXErrorSnackBar(
  119. title: Strings.error.tr,
  120. message: Strings.unableToConnectServer.tr,
  121. );
  122. }
  123. } else {
  124. IXSnackBar.showIXErrorSnackBar(
  125. title: Strings.error.tr,
  126. message: error.toString(),
  127. );
  128. }
  129. }
  130. void _handleCancel() {
  131. setState(() {
  132. _isCancelled = true;
  133. });
  134. Navigator.of(context).pop();
  135. widget.onCancel?.call();
  136. }
  137. @override
  138. Widget build(BuildContext context) {
  139. return WillPopScope(
  140. onWillPop: () async => widget.canCancel, // 根据canCancel决定是否可以返回
  141. child: Dialog(
  142. backgroundColor: Get.reactiveTheme.scaffoldBackgroundColor,
  143. shape: RoundedRectangleBorder(
  144. borderRadius: BorderRadius.circular(16),
  145. // side: BorderSide(color: Palette.white.withOpacity(0.12)),
  146. ),
  147. child: Container(
  148. padding: const EdgeInsets.all(24),
  149. decoration: BoxDecoration(
  150. color: Get.reactiveTheme.scaffoldBackgroundColor,
  151. borderRadius: BorderRadius.circular(16),
  152. ),
  153. child: Column(
  154. mainAxisSize: MainAxisSize.min,
  155. children: [
  156. Stack(
  157. alignment: Alignment.center,
  158. children: [
  159. SizedBox(
  160. width: 46.w,
  161. height: 46.w,
  162. child: Lottie.asset(
  163. 'assets/lottie/loading.json',
  164. controller: _controller,
  165. frameRate: FrameRate.max,
  166. onLoaded: (composition) {
  167. _controller.duration = _loadingDuration;
  168. _controller.addListener(() {
  169. if (_state == LoadingState.loading &&
  170. _controller.value >= 0.5) {
  171. _controller.value = 0.0;
  172. _controller.forward();
  173. }
  174. });
  175. _controller.forward();
  176. },
  177. ),
  178. ),
  179. ],
  180. ),
  181. 16.verticalSpaceFromWidth,
  182. Text(
  183. (_state == LoadingState.loading
  184. ? (widget.loadingText ?? Strings.processing.tr)
  185. : (widget.successText ?? Strings.success.tr)),
  186. style: TextStyle(
  187. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  188. fontSize: 12.sp,
  189. fontWeight: FontWeight.w500,
  190. ),
  191. ),
  192. if (widget.canCancel)
  193. Column(
  194. children: [
  195. 16.verticalSpaceFromWidth,
  196. SubmitButton(
  197. text: Strings.cancel.tr,
  198. onPressed: _handleCancel,
  199. ),
  200. ],
  201. ),
  202. ],
  203. ),
  204. ),
  205. ),
  206. );
  207. }
  208. }