299 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Dart
		
	
	
| // custom_alert_dialog.dart
 | ||
| import 'package:flutter/material.dart';
 | ||
| import 'package:qhd_prevention/main.dart'; // 导入全局 navigatorKey
 | ||
| 
 | ||
| /// 对话框模式
 | ||
| enum DialogMode { text, input }
 | ||
| 
 | ||
| class CustomAlertDialog extends StatefulWidget {
 | ||
|   final String title;
 | ||
|   final String content;
 | ||
|   final String hintText;
 | ||
|   final String cancelText;
 | ||
|   final String confirmText;
 | ||
|   final VoidCallback? onCancel;
 | ||
|   final VoidCallback? onConfirm;
 | ||
|   final ValueChanged<String>? onInputConfirm;
 | ||
|   final DialogMode mode;
 | ||
|   final bool force;
 | ||
| 
 | ||
|   const CustomAlertDialog({
 | ||
|     Key? key,
 | ||
|     required this.title,
 | ||
|     this.content = '',
 | ||
|     this.hintText = '',
 | ||
|     this.cancelText = '取消',
 | ||
|     this.confirmText = '确定',
 | ||
|     this.onCancel,
 | ||
|     this.onConfirm,
 | ||
|     this.onInputConfirm,
 | ||
|     this.mode = DialogMode.text,
 | ||
|     this.force = false,
 | ||
|   }) : super(key: key);
 | ||
| 
 | ||
|   // ------------------ 静态快捷方法 ------------------
 | ||
| 
 | ||
|   static Future<bool> showConfirm(
 | ||
|       BuildContext context, {
 | ||
|         required String title,
 | ||
|         String content = '',
 | ||
|         String cancelText = '取消',
 | ||
|         String confirmText = '确定',
 | ||
|         bool barrierDismissible = true,
 | ||
|         VoidCallback? onConfirm,
 | ||
|         bool force = false,
 | ||
|       }) async {
 | ||
|     final result = await showDialog<bool>(
 | ||
|       context: context,
 | ||
|       barrierDismissible: force ? false : barrierDismissible,
 | ||
|       builder: (_) => CustomAlertDialog(
 | ||
|         title: title,
 | ||
|         content: content,
 | ||
|         cancelText: cancelText,
 | ||
|         confirmText: confirmText,
 | ||
|         onConfirm: onConfirm,
 | ||
|         force: force,
 | ||
|       ),
 | ||
|     );
 | ||
|     return result == true;
 | ||
|   }
 | ||
| 
 | ||
|   static Future<void> showAlert(
 | ||
|       BuildContext context, {
 | ||
|         required String title,
 | ||
|         String content = '',
 | ||
|         String confirmText = '确定',
 | ||
|         bool barrierDismissible = true,
 | ||
|         VoidCallback? onConfirm,
 | ||
|         bool force = false,
 | ||
|       }) async {
 | ||
|     await showDialog<void>(
 | ||
|       context: context,
 | ||
|       barrierDismissible: force ? false : barrierDismissible,
 | ||
|       builder: (_) => CustomAlertDialog(
 | ||
|         title: title,
 | ||
|         content: content,
 | ||
|         cancelText: '',
 | ||
|         confirmText: confirmText,
 | ||
|         onConfirm: onConfirm,
 | ||
|         force: force,
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   static Future<String?> showInput(
 | ||
|       BuildContext context, {
 | ||
|         required String title,
 | ||
|         String hintText = '',
 | ||
|         String cancelText = '取消',
 | ||
|         String confirmText = '确定',
 | ||
|         bool barrierDismissible = true,
 | ||
|         bool force = false,
 | ||
|       }) async {
 | ||
|     final result = await showDialog<String?>(
 | ||
|       context: context,
 | ||
|       barrierDismissible: force ? false : barrierDismissible,
 | ||
|       builder: (_) => CustomAlertDialog(
 | ||
|         title: title,
 | ||
|         hintText: hintText,
 | ||
|         cancelText: cancelText,
 | ||
|         confirmText: confirmText,
 | ||
|         mode: DialogMode.input,
 | ||
|         force: force,
 | ||
|       ),
 | ||
|     );
 | ||
|     // 取消/点遮罩会得到 null;确认会得到 String(可能为空串)
 | ||
|     return result;
 | ||
|   }
 | ||
| 
 | ||
| 
 | ||
|   @override
 | ||
|   _CustomAlertDialogState createState() => _CustomAlertDialogState();
 | ||
| }
 | ||
| 
 | ||
| class _CustomAlertDialogState extends State<CustomAlertDialog> {
 | ||
|   late TextEditingController _controller;
 | ||
|   bool _isClosing = false;
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|     _controller = TextEditingController();
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   void dispose() {
 | ||
|     _controller.dispose();
 | ||
|     super.dispose();
 | ||
|   }
 | ||
| 
 | ||
|   bool get hasCancel => !widget.force && widget.cancelText.isNotEmpty;
 | ||
| 
 | ||
|   void _closeDialog([dynamic result]) {
 | ||
|     if (_isClosing) return;
 | ||
|     _isClosing = true;
 | ||
| 
 | ||
|     // 优先使用当前上下文导航
 | ||
|     if (mounted) {
 | ||
|       Navigator.of(context).pop(result);
 | ||
|     } else {
 | ||
|       // 后备方案:使用全局导航键
 | ||
|       navigatorKey.currentState?.pop(result);
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     return PopScope(
 | ||
|       canPop: !widget.force,
 | ||
|       child: Dialog(
 | ||
|         backgroundColor: Colors.transparent,
 | ||
|         child: Container(
 | ||
|           constraints: const BoxConstraints(minWidth: 280),
 | ||
|           decoration: BoxDecoration(
 | ||
|             color: Colors.white,
 | ||
|             borderRadius: BorderRadius.circular(8),
 | ||
|           ),
 | ||
|           child: Column(
 | ||
|             mainAxisSize: MainAxisSize.min,
 | ||
|             children: [
 | ||
|               const SizedBox(height: 20),
 | ||
|               Padding(
 | ||
|                 padding: const EdgeInsets.symmetric(horizontal: 20),
 | ||
|                 child: Text(
 | ||
|                   widget.title,
 | ||
|                   style: const TextStyle(
 | ||
|                     fontSize: 18,
 | ||
|                     fontWeight: FontWeight.bold,
 | ||
|                   ),
 | ||
|                   textAlign: TextAlign.center,
 | ||
|                 ),
 | ||
|               ),
 | ||
|               const SizedBox(height: 16),
 | ||
|               if (widget.mode == DialogMode.text)
 | ||
|                 Padding(
 | ||
|                   padding: const EdgeInsets.symmetric(horizontal: 24),
 | ||
|                   child: Text(
 | ||
|                     widget.content,
 | ||
|                     style: const TextStyle(
 | ||
|                       fontSize: 16,
 | ||
|                       color: Colors.black54,
 | ||
|                     ),
 | ||
|                     textAlign: TextAlign.center,
 | ||
|                   ),
 | ||
|                 )
 | ||
|               else
 | ||
|                 Padding(
 | ||
|                   padding: const EdgeInsets.symmetric(horizontal: 20),
 | ||
|                   child: TextField(
 | ||
|                     controller: _controller,
 | ||
|                     autofocus: true,
 | ||
|                     decoration: InputDecoration(
 | ||
|                       hintText: widget.hintText,
 | ||
|                       border: const OutlineInputBorder(),
 | ||
|                       focusedBorder: OutlineInputBorder(
 | ||
|                         borderSide: BorderSide(color: Colors.blue, width: 1),
 | ||
|                         borderRadius: BorderRadius.circular(4),
 | ||
|                       ),
 | ||
|                       isDense: true,
 | ||
|                       contentPadding: const EdgeInsets.symmetric(
 | ||
|                         vertical: 10,
 | ||
|                         horizontal: 10,
 | ||
|                       ),
 | ||
|                     ),
 | ||
|                   ),
 | ||
|                 ),
 | ||
|               const SizedBox(height: 20),
 | ||
|               const Divider(height: 1),
 | ||
|               hasCancel
 | ||
|                   ? _buildDoubleButtons(context)
 | ||
|                   : _buildSingleButton(context),
 | ||
|             ],
 | ||
|           ),
 | ||
|         ),
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   Widget _buildDoubleButtons(BuildContext context) {
 | ||
|     return Row(
 | ||
|       children: [
 | ||
|         Expanded(
 | ||
|           child: InkWell(
 | ||
|             onTap: () {
 | ||
|               widget.onCancel?.call();
 | ||
|               _closeDialog(widget.mode == DialogMode.text ? false : null);
 | ||
|             },
 | ||
|             child: Container(
 | ||
|               padding: const EdgeInsets.symmetric(vertical: 12),
 | ||
|               alignment: Alignment.center,
 | ||
|               child: Text(
 | ||
|                 widget.cancelText,
 | ||
|                 style: const TextStyle(
 | ||
|                   fontWeight: FontWeight.w500,
 | ||
|                   color: Colors.black87,
 | ||
|                   fontSize: 18,
 | ||
|                 ),
 | ||
|               ),
 | ||
|             ),
 | ||
|           ),
 | ||
|         ),
 | ||
|         Container(width: 1, height: 48, color: Colors.grey[300]),
 | ||
|         Expanded(
 | ||
|           child: InkWell(
 | ||
|             onTap: () {
 | ||
|               if (widget.mode == DialogMode.text) {
 | ||
|                 widget.onConfirm?.call();
 | ||
|                 _closeDialog(true);
 | ||
|               } else {
 | ||
|                 final value = _controller.text.trim();
 | ||
|                 widget.onInputConfirm?.call(value);
 | ||
|                 _closeDialog(value);
 | ||
|               }
 | ||
|             },
 | ||
|             child: Container(
 | ||
|               padding: const EdgeInsets.symmetric(vertical: 12),
 | ||
|               alignment: Alignment.center,
 | ||
|               child: Text(
 | ||
|                 widget.confirmText,
 | ||
|                 style: const TextStyle(
 | ||
|                   color: Color(0xFF3874F6),
 | ||
|                   fontWeight: FontWeight.w500,
 | ||
|                   fontSize: 18,
 | ||
|                 ),
 | ||
|               ),
 | ||
|             ),
 | ||
|           ),
 | ||
|         ),
 | ||
|       ],
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   Widget _buildSingleButton(BuildContext context) {
 | ||
|     return InkWell(
 | ||
|       onTap: () {
 | ||
|         if (widget.mode == DialogMode.text) {
 | ||
|           widget.onConfirm?.call();
 | ||
|           _closeDialog(true);
 | ||
|         } else {
 | ||
|           final value = _controller.text.trim();
 | ||
|           widget.onInputConfirm?.call(value);
 | ||
|           _closeDialog(value);
 | ||
|         }
 | ||
|       },
 | ||
|       child: Container(
 | ||
|         width: double.infinity,
 | ||
|         padding: const EdgeInsets.symmetric(vertical: 14),
 | ||
|         alignment: Alignment.center,
 | ||
|         child: Text(
 | ||
|           widget.confirmText,
 | ||
|           style: const TextStyle(
 | ||
|             color: Color(0xFF3874F6),
 | ||
|             fontWeight: FontWeight.w500,
 | ||
|             fontSize: 18,
 | ||
|           ),
 | ||
|         ),
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| } |