custom_dialog.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:flutter_svg/flutter_svg.dart';
  4. import 'package:get/get.dart';
  5. import 'package:nomo/app/widgets/click_opacity.dart';
  6. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  7. /// 通用弹窗组件
  8. class CustomDialog {
  9. /// 显示成功弹窗(如Premium激活成功)
  10. static void showSuccess({
  11. required String title,
  12. required String message,
  13. String buttonText = 'Got it',
  14. VoidCallback? onPressed,
  15. IconData? icon,
  16. String? svgPath,
  17. Color? iconColor,
  18. Color? titleColor,
  19. Color? messageColor,
  20. }) {
  21. Get.generalDialog(
  22. pageBuilder: (context, animation, secondaryAnimation) {
  23. return _CustomDialogWidget(
  24. type: DialogType.success,
  25. title: title,
  26. message: message,
  27. buttonText: buttonText,
  28. onPressed: onPressed,
  29. icon: icon ?? Icons.workspace_premium,
  30. svgPath: svgPath,
  31. iconColor: iconColor ?? const Color(0xFFFF9500),
  32. titleColor: titleColor,
  33. messageColor: messageColor,
  34. animation: animation,
  35. );
  36. },
  37. barrierDismissible: false,
  38. transitionDuration: const Duration(milliseconds: 300),
  39. );
  40. }
  41. static void showUpdateDialog({
  42. required String title,
  43. required String message,
  44. String buttonText = 'Retry',
  45. String? cancelText,
  46. VoidCallback? onPressed,
  47. VoidCallback? onCancel,
  48. String? svgPath,
  49. Color? iconColor,
  50. Color? titleColor,
  51. Color? messageColor,
  52. Color? confirmButtonColor,
  53. Color? cancelButtonColor,
  54. }) {
  55. Get.generalDialog(
  56. pageBuilder: (context, animation, secondaryAnimation) {
  57. return WillPopScope(
  58. onWillPop: () async => false,
  59. child: _CustomDialogWidget(
  60. type: DialogType.error,
  61. title: title,
  62. message: message,
  63. buttonText: buttonText,
  64. cancelText: cancelText,
  65. onPressed: onPressed,
  66. onCancel: onCancel,
  67. svgPath: svgPath,
  68. iconColor:
  69. iconColor ?? Get.reactiveTheme.textTheme.bodyLarge!.color!,
  70. titleColor: titleColor,
  71. messageColor: messageColor,
  72. confirmButtonColor: confirmButtonColor,
  73. cancelButtonColor: cancelButtonColor,
  74. animation: animation,
  75. ),
  76. );
  77. },
  78. barrierDismissible: false,
  79. transitionDuration: const Duration(milliseconds: 300),
  80. );
  81. }
  82. /// 显示信息弹窗(如邮件发送成功)
  83. static void showInfo({
  84. required String title,
  85. required String message,
  86. String buttonText = 'OK',
  87. VoidCallback? onPressed,
  88. IconData? icon,
  89. String? svgPath,
  90. Color? iconColor,
  91. Color? titleColor,
  92. Color? messageColor,
  93. }) {
  94. Get.generalDialog(
  95. pageBuilder: (context, animation, secondaryAnimation) {
  96. return _CustomDialogWidget(
  97. type: DialogType.info,
  98. title: title,
  99. message: message,
  100. buttonText: buttonText,
  101. onPressed: onPressed,
  102. icon: icon ?? Icons.mark_email_read,
  103. svgPath: svgPath,
  104. iconColor: iconColor ?? const Color(0xFF00A8E8),
  105. titleColor: titleColor,
  106. messageColor: messageColor,
  107. animation: animation,
  108. );
  109. },
  110. barrierDismissible: false,
  111. transitionDuration: const Duration(milliseconds: 300),
  112. );
  113. }
  114. /// 显示错误弹窗(如网络连接错误)
  115. static void showError({
  116. required String title,
  117. required String message,
  118. String buttonText = 'Retry',
  119. String? cancelText,
  120. VoidCallback? onPressed,
  121. VoidCallback? onCancel,
  122. IconData? icon,
  123. String? svgPath,
  124. Color? iconColor,
  125. Color? titleColor,
  126. Color? messageColor,
  127. Color? confirmButtonColor,
  128. Color? cancelButtonColor,
  129. String? errorCode,
  130. }) {
  131. Get.generalDialog(
  132. pageBuilder: (context, animation, secondaryAnimation) {
  133. return _CustomDialogWidget(
  134. type: DialogType.error,
  135. title: title,
  136. message: message,
  137. buttonText: buttonText,
  138. cancelText: cancelText,
  139. onPressed: onPressed,
  140. onCancel: onCancel,
  141. icon: icon ?? Icons.wifi_off,
  142. svgPath: svgPath,
  143. iconColor: iconColor ?? const Color(0xFFFF9500),
  144. titleColor: titleColor,
  145. messageColor: messageColor,
  146. confirmButtonColor:
  147. confirmButtonColor ?? Get.reactiveTheme.primaryColor,
  148. cancelButtonColor: cancelButtonColor,
  149. errorCode: errorCode,
  150. animation: animation,
  151. );
  152. },
  153. barrierDismissible: false,
  154. transitionDuration: const Duration(milliseconds: 300),
  155. );
  156. }
  157. /// 显示确认弹窗(如退出登录确认)
  158. static void showConfirm({
  159. required String title,
  160. required String message,
  161. String confirmText = 'Confirm',
  162. String cancelText = 'Cancel',
  163. required VoidCallback onConfirm,
  164. VoidCallback? onCancel,
  165. IconData? icon,
  166. String? svgPath,
  167. Color? iconColor,
  168. Color? titleColor,
  169. Color? messageColor,
  170. Color? confirmButtonColor,
  171. Color? cancelButtonColor,
  172. }) {
  173. Get.generalDialog(
  174. pageBuilder: (context, animation, secondaryAnimation) {
  175. return _CustomDialogWidget(
  176. type: DialogType.confirm,
  177. title: title,
  178. message: message,
  179. buttonText: confirmText, // 添加必需的参数
  180. confirmText: confirmText,
  181. cancelText: cancelText,
  182. onConfirm: onConfirm,
  183. onCancel: onCancel,
  184. icon: icon ?? Icons.info_outline,
  185. svgPath: svgPath,
  186. iconColor: iconColor ?? const Color(0xFFFF3B30),
  187. titleColor: titleColor,
  188. messageColor: messageColor,
  189. confirmButtonColor: confirmButtonColor ?? const Color(0xFFFF3B30),
  190. cancelButtonColor: cancelButtonColor,
  191. animation: animation,
  192. );
  193. },
  194. barrierDismissible: true,
  195. barrierLabel: 'Dismiss',
  196. transitionDuration: const Duration(milliseconds: 300),
  197. );
  198. }
  199. }
  200. /// 弹窗类型枚举
  201. enum DialogType { success, info, error, confirm }
  202. /// 自定义弹窗组件
  203. class _CustomDialogWidget extends StatelessWidget {
  204. final DialogType type;
  205. final String title;
  206. final String message;
  207. final String buttonText;
  208. final String? confirmText;
  209. final String? cancelText;
  210. final VoidCallback? onPressed;
  211. final VoidCallback? onConfirm;
  212. final VoidCallback? onCancel;
  213. final IconData? icon;
  214. final String? svgPath;
  215. final Color iconColor;
  216. final Color? titleColor;
  217. final Color? messageColor;
  218. final Color? confirmButtonColor;
  219. final Color? cancelButtonColor;
  220. final String? errorCode;
  221. final Animation<double>? animation;
  222. const _CustomDialogWidget({
  223. required this.type,
  224. required this.title,
  225. required this.message,
  226. required this.buttonText,
  227. this.confirmText,
  228. this.cancelText,
  229. this.onPressed,
  230. this.onConfirm,
  231. this.onCancel,
  232. this.icon,
  233. this.svgPath,
  234. required this.iconColor,
  235. this.titleColor,
  236. this.messageColor,
  237. this.confirmButtonColor,
  238. this.cancelButtonColor,
  239. this.errorCode,
  240. this.animation,
  241. });
  242. @override
  243. Widget build(BuildContext context) {
  244. if (animation == null) {
  245. // 如果没有动画,返回普通弹窗
  246. return _buildDialog();
  247. }
  248. // 使用动画构建弹窗
  249. return _buildAnimatedDialog();
  250. }
  251. /// 构建普通弹窗
  252. Widget _buildDialog() {
  253. return Dialog(
  254. backgroundColor: Colors.transparent,
  255. insetPadding: EdgeInsets.symmetric(horizontal: 24.w),
  256. child: _buildDialogContent(),
  257. );
  258. }
  259. /// 构建带动画的弹窗
  260. Widget _buildAnimatedDialog() {
  261. return Dialog(
  262. backgroundColor: Colors.transparent,
  263. insetPadding: EdgeInsets.symmetric(horizontal: 24.w),
  264. child: AnimatedBuilder(
  265. animation: animation!,
  266. builder: (context, child) {
  267. // 使用不同的曲线来避免关闭时的抖动
  268. final scaleValue = Tween<double>(begin: 0.8, end: 1.0)
  269. .animate(
  270. CurvedAnimation(
  271. parent: animation!,
  272. curve: Curves.easeOutCubic,
  273. reverseCurve: Curves.easeInCubic,
  274. ),
  275. )
  276. .value;
  277. final offsetValue = Tween<double>(begin: 30, end: 0)
  278. .animate(
  279. CurvedAnimation(
  280. parent: animation!,
  281. curve: Curves.easeOutCubic,
  282. reverseCurve: Curves.easeInCubic,
  283. ),
  284. )
  285. .value;
  286. return Transform.scale(
  287. scale: scaleValue,
  288. child: Transform.translate(
  289. offset: Offset(0, offsetValue),
  290. child: Opacity(opacity: animation!.value, child: child),
  291. ),
  292. );
  293. },
  294. child: _buildDialogContent(),
  295. ),
  296. );
  297. }
  298. /// 构建弹窗内容
  299. Widget _buildDialogContent() {
  300. return Container(
  301. padding: EdgeInsets.all(24.w),
  302. decoration: BoxDecoration(
  303. color: Get.reactiveTheme.highlightColor,
  304. borderRadius: BorderRadius.circular(16.r),
  305. ),
  306. child: Column(
  307. mainAxisSize: MainAxisSize.min,
  308. crossAxisAlignment: CrossAxisAlignment.start,
  309. children: [
  310. // 图标
  311. _buildIcon(),
  312. // 标题和消息
  313. _buildContent(),
  314. 24.verticalSpaceFromWidth,
  315. // 按钮
  316. _buildButtons(),
  317. ],
  318. ),
  319. );
  320. }
  321. /// 构建图标
  322. Widget _buildIcon() {
  323. return Container(
  324. margin: EdgeInsets.only(bottom: 10.w),
  325. child: svgPath != null
  326. ? SvgPicture.asset(
  327. svgPath!,
  328. width: 40.w,
  329. height: 40.w,
  330. colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn),
  331. )
  332. : Icon(icon ?? Icons.info_outline, size: 40.w, color: iconColor),
  333. );
  334. }
  335. /// 构建内容区域
  336. Widget _buildContent() {
  337. return Column(
  338. crossAxisAlignment: CrossAxisAlignment.start,
  339. children: [
  340. // 标题
  341. Text(
  342. title,
  343. style: TextStyle(
  344. fontSize: 22.sp,
  345. fontWeight: FontWeight.w500,
  346. color: titleColor ?? Get.reactiveTheme.textTheme.bodyLarge!.color,
  347. ),
  348. ),
  349. 18.verticalSpaceFromWidth,
  350. // 消息
  351. Text(
  352. message,
  353. style: TextStyle(
  354. fontSize: 14.sp,
  355. color: messageColor ?? Get.reactiveTheme.hintColor,
  356. height: 1.4,
  357. ),
  358. ),
  359. ],
  360. );
  361. }
  362. /// 构建按钮区域
  363. Widget _buildButtons() {
  364. switch (type) {
  365. case DialogType.success:
  366. case DialogType.info:
  367. return _buildSingleButton();
  368. case DialogType.error:
  369. return _buildErrorButtons();
  370. case DialogType.confirm:
  371. return _buildConfirmButtons();
  372. }
  373. }
  374. /// 构建单个按钮
  375. Widget _buildSingleButton() {
  376. return SizedBox(
  377. width: double.infinity,
  378. height: 42.w,
  379. child: ClickOpacity(
  380. onTap: () {
  381. onPressed?.call();
  382. },
  383. child: Container(
  384. decoration: BoxDecoration(
  385. color: confirmButtonColor ?? Get.reactiveTheme.primaryColor,
  386. borderRadius: BorderRadius.circular(8.r),
  387. ),
  388. alignment: Alignment.center,
  389. child: Text(
  390. buttonText,
  391. style: TextStyle(
  392. fontSize: 14.sp,
  393. fontWeight: FontWeight.w600,
  394. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  395. ),
  396. ),
  397. ),
  398. ),
  399. );
  400. }
  401. /// 构建错误弹窗按钮
  402. Widget _buildErrorButtons() {
  403. return Column(
  404. children: [
  405. SizedBox(
  406. width: double.infinity,
  407. height: 42.h,
  408. child: ClickOpacity(
  409. onTap: () {
  410. onPressed?.call();
  411. },
  412. child: Container(
  413. decoration: BoxDecoration(
  414. color: confirmButtonColor ?? Get.reactiveTheme.primaryColor,
  415. borderRadius: BorderRadius.circular(8.r),
  416. ),
  417. alignment: Alignment.center,
  418. child: Text(
  419. buttonText,
  420. style: TextStyle(
  421. fontSize: 14.sp,
  422. fontWeight: FontWeight.w600,
  423. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  424. ),
  425. ),
  426. ),
  427. ),
  428. ),
  429. if (cancelText != null) ...[
  430. SizedBox(height: 12.w),
  431. SizedBox(
  432. width: double.infinity,
  433. height: 42.h,
  434. child: ClickOpacity(
  435. onTap: () {
  436. onCancel?.call();
  437. },
  438. child: Container(
  439. decoration: BoxDecoration(
  440. borderRadius: BorderRadius.circular(8.r),
  441. border: Border.all(
  442. color: cancelButtonColor ?? Get.reactiveTheme.dividerColor,
  443. width: 1.w,
  444. ),
  445. ),
  446. alignment: Alignment.center,
  447. child: Text(
  448. cancelText!,
  449. style: TextStyle(
  450. fontSize: 14.sp,
  451. fontWeight: FontWeight.w600,
  452. color: cancelButtonColor ?? Get.reactiveTheme.hintColor,
  453. ),
  454. ),
  455. ),
  456. ),
  457. ),
  458. ],
  459. ],
  460. );
  461. }
  462. /// 构建确认弹窗按钮
  463. Widget _buildConfirmButtons() {
  464. return Column(
  465. children: [
  466. SizedBox(
  467. width: double.infinity,
  468. height: 42.w,
  469. child: ClickOpacity(
  470. onTap: () {
  471. onConfirm?.call();
  472. },
  473. child: Container(
  474. decoration: BoxDecoration(
  475. color: confirmButtonColor ?? iconColor,
  476. borderRadius: BorderRadius.circular(8.r),
  477. ),
  478. alignment: Alignment.center,
  479. child: Text(
  480. confirmText ?? 'Confirm',
  481. style: TextStyle(
  482. fontSize: 14.sp,
  483. fontWeight: FontWeight.w600,
  484. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  485. ),
  486. ),
  487. ),
  488. ),
  489. ),
  490. 8.verticalSpace,
  491. SizedBox(
  492. width: double.infinity,
  493. height: 42.w,
  494. child: ClickOpacity(
  495. onTap: () {
  496. onCancel?.call();
  497. },
  498. child: Container(
  499. decoration: BoxDecoration(
  500. borderRadius: BorderRadius.circular(8.r),
  501. border: Border.all(
  502. color: cancelButtonColor ?? Get.reactiveTheme.dividerColor,
  503. width: 1.w,
  504. ),
  505. ),
  506. alignment: Alignment.center,
  507. child: Text(
  508. cancelText ?? 'Cancel',
  509. style: TextStyle(
  510. fontSize: 14.sp,
  511. fontWeight: FontWeight.w600,
  512. color: cancelButtonColor ?? Get.reactiveTheme.hintColor,
  513. ),
  514. ),
  515. ),
  516. ),
  517. ),
  518. ],
  519. );
  520. }
  521. }