264 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Dart
		
	
	
		
		
			
		
	
	
			264 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Dart
		
	
	
|  | import 'package:flutter/material.dart'; | |||
|  | import 'package:qhd_prevention/customWidget/custom_button.dart'; | |||
|  | import 'package:qhd_prevention/customWidget/toast_util.dart'; | |||
|  | 
 | |||
|  | /// 居中多选弹窗(Dialog)
 | |||
|  | /// 返回 Future<List<T>?>:用户点击确定返回所选项列表;取消或关闭返回 null。
 | |||
|  | class CenterMultiPicker { | |||
|  |   static Future<List<T>?> show<T>( | |||
|  |     BuildContext context, { | |||
|  |     required List<T> items, | |||
|  |     required Widget Function(T item) itemBuilder, | |||
|  |     List<int>? initialSelectedIndices, | |||
|  |     int? maxSelection, | |||
|  |     bool allowEmpty = false, | |||
|  |     double itemHeight = 52, | |||
|  |     double maxHeightFactor = 0.75, // 屏幕高度的最大占比
 | |||
|  |     String? title, | |||
|  |   }) { | |||
|  |     if (items.isEmpty) return Future.value(null); | |||
|  | 
 | |||
|  |     // 安全化初始索引
 | |||
|  |     final initialSet = <int>{}; | |||
|  |     if (initialSelectedIndices != null) { | |||
|  |       for (final i in initialSelectedIndices) { | |||
|  |         // if (i >= 0 && i < items.length) initialSet.add(i);
 | |||
|  |       } | |||
|  |     } | |||
|  | 
 | |||
|  |     return showDialog<List<T>?>( | |||
|  |       context: context, | |||
|  |       barrierDismissible: true, | |||
|  |       builder: (ctx) { | |||
|  |         return Dialog( | |||
|  |           shape: RoundedRectangleBorder( | |||
|  |             borderRadius: BorderRadius.circular(12), | |||
|  |           ), | |||
|  |           insetPadding: const EdgeInsets.symmetric( | |||
|  |             horizontal: 24, | |||
|  |             vertical: 24, | |||
|  |           ), | |||
|  |           child: _CenterMultiPickerBody<T>( | |||
|  |             items: items, | |||
|  |             itemBuilder: itemBuilder, | |||
|  |             initialSelected: initialSet, | |||
|  |             maxSelection: maxSelection, | |||
|  |             allowEmpty: allowEmpty, | |||
|  |             itemHeight: itemHeight, | |||
|  |             maxHeightFactor: maxHeightFactor, | |||
|  |             title: title, | |||
|  |           ), | |||
|  |         ); | |||
|  |       }, | |||
|  |     ); | |||
|  |   } | |||
|  | } | |||
|  | 
 | |||
|  | class _CenterMultiPickerBody<T> extends StatefulWidget { | |||
|  |   const _CenterMultiPickerBody({ | |||
|  |     Key? key, | |||
|  |     required this.items, | |||
|  |     required this.itemBuilder, | |||
|  |     required this.initialSelected, | |||
|  |     required this.maxSelection, | |||
|  |     required this.allowEmpty, | |||
|  |     required this.itemHeight, | |||
|  |     required this.maxHeightFactor, | |||
|  |     this.title, | |||
|  |   }) : super(key: key); | |||
|  | 
 | |||
|  |   final List<T> items; | |||
|  |   final Widget Function(T item) itemBuilder; | |||
|  |   final Set<int> initialSelected; | |||
|  |   final int? maxSelection; | |||
|  |   final bool allowEmpty; | |||
|  |   final double itemHeight; | |||
|  |   final double maxHeightFactor; | |||
|  |   final String? title; | |||
|  | 
 | |||
|  |   @override | |||
|  |   State<_CenterMultiPickerBody<T>> createState() => | |||
|  |       _CenterMultiPickerBodyState<T>(); | |||
|  | } | |||
|  | 
 | |||
|  | class _CenterMultiPickerBodyState<T> extends State<_CenterMultiPickerBody<T>> { | |||
|  |   late Set<int> _selected; | |||
|  | 
 | |||
|  |   // 固定的 header / footer 高度估算
 | |||
|  |   static const double _headerHeight = 56; | |||
|  |   static const double _footerHeight = 58; | |||
|  |   static const double _verticalPadding = 16; // Dialog 内上下 padding
 | |||
|  | 
 | |||
|  |   @override | |||
|  |   void initState() { | |||
|  |     super.initState(); | |||
|  |     _selected = Set<int>.from(widget.initialSelected); | |||
|  |   } | |||
|  | 
 | |||
|  |   void _toggle(int idx) { | |||
|  |     setState(() { | |||
|  |       if (_selected.contains(idx)) { | |||
|  |         _selected.remove(idx); | |||
|  |       } else { | |||
|  |         if (widget.maxSelection != null && | |||
|  |             _selected.length >= widget.maxSelection!) { | |||
|  |           ToastUtil.showNormal(context, '最多可选择 ${widget.maxSelection} 项'); | |||
|  |           return; | |||
|  |         } | |||
|  |         _selected.add(idx); | |||
|  |       } | |||
|  |     }); | |||
|  |   } | |||
|  | 
 | |||
|  |   @override | |||
|  |   Widget build(BuildContext context) { | |||
|  |     final items = widget.items; | |||
|  |     final screenH = MediaQuery.of(context).size.height; | |||
|  |     final contentHeight = | |||
|  |         items.length * widget.itemHeight + | |||
|  |         _headerHeight + | |||
|  |         _footerHeight + | |||
|  |         _verticalPadding * 2; | |||
|  |     final maxAllowed = screenH * widget.maxHeightFactor; | |||
|  |     final dialogHeight = | |||
|  |         contentHeight <= maxAllowed ? contentHeight : maxAllowed; | |||
|  | 
 | |||
|  |     return Container( | |||
|  |       decoration: BoxDecoration( | |||
|  |           color: Colors.white, | |||
|  |           borderRadius: BorderRadius.circular(10) | |||
|  |       ), | |||
|  |       width: double.infinity, | |||
|  |       height: dialogHeight, | |||
|  |       child: Column( | |||
|  |         crossAxisAlignment: CrossAxisAlignment.center, | |||
|  |         mainAxisAlignment: MainAxisAlignment.center, | |||
|  |         children: [ | |||
|  |           // header
 | |||
|  |           Container( | |||
|  |             height: _headerHeight, | |||
|  |             padding: const EdgeInsets.symmetric(horizontal: 16), | |||
|  |             alignment: Alignment.centerLeft, | |||
|  |             child: Row( | |||
|  |               children: [ | |||
|  |                 if (widget.title != null) | |||
|  |                   Expanded( | |||
|  |                     child: Text( | |||
|  |                       widget.title!, | |||
|  |                       style: const TextStyle( | |||
|  |                         fontSize: 15, | |||
|  |                         fontWeight: FontWeight.w600, | |||
|  |                       ), | |||
|  |                     ), | |||
|  |                   ) | |||
|  |                 else | |||
|  |                   Expanded( | |||
|  |                     child: Text( | |||
|  |                       '已选 ${_selected.length}${widget.maxSelection != null ? '/${widget.maxSelection}' : ''}', | |||
|  |                       style: const TextStyle(fontSize: 15), | |||
|  |                     ), | |||
|  |                   ), | |||
|  |                 // 可将一些快捷按钮放在右侧(如全选/反选),这里暂不显示
 | |||
|  |               ], | |||
|  |             ), | |||
|  |           ), | |||
|  |           const Divider(height: 1), | |||
|  |           // 列表区域(可滚动)
 | |||
|  |           Expanded( | |||
|  |             child: Container( | |||
|  |               padding: const EdgeInsets.symmetric(vertical: 8), | |||
|  |               child: Scrollbar( | |||
|  |                 thumbVisibility: true, | |||
|  |                 child: ListView.separated( | |||
|  |                   physics: const BouncingScrollPhysics(), | |||
|  |                   itemCount: items.length, | |||
|  |                   separatorBuilder: (_, __) => const Divider(height: 1), | |||
|  |                   itemBuilder: (ctx, idx) { | |||
|  |                     final isSelected = _selected.contains(idx); | |||
|  |                     return InkWell( | |||
|  |                       onTap: () => _toggle(idx), | |||
|  |                       child: Container( | |||
|  |                         height: widget.itemHeight, | |||
|  |                         padding: const EdgeInsets.symmetric( | |||
|  |                           horizontal: 14, | |||
|  |                           vertical: 6, | |||
|  |                         ), | |||
|  |                         child: Row( | |||
|  |                           children: [ | |||
|  |                             Container( | |||
|  |                               width: 22, | |||
|  |                               height: 22, | |||
|  |                               decoration: BoxDecoration( | |||
|  |                                 color: | |||
|  |                                     isSelected | |||
|  |                                         ? Colors.blue | |||
|  |                                         : Colors.transparent, | |||
|  |                                 border: Border.all( | |||
|  |                                   color: | |||
|  |                                       isSelected ? Colors.blue : Colors.black26, | |||
|  |                                 ), | |||
|  |                                 borderRadius: BorderRadius.circular(4), | |||
|  |                               ), | |||
|  |                               child: | |||
|  |                                   isSelected | |||
|  |                                       ? const Icon( | |||
|  |                                         Icons.check, | |||
|  |                                         size: 18, | |||
|  |                                         color: Colors.white, | |||
|  |                                       ) | |||
|  |                                       : null, | |||
|  |                             ), | |||
|  |                             const SizedBox(width: 12), | |||
|  |                             Expanded(child: widget.itemBuilder(items[idx])), | |||
|  |                           ], | |||
|  |                         ), | |||
|  |                       ), | |||
|  |                     ); | |||
|  |                   }, | |||
|  |                 ), | |||
|  |               ), | |||
|  |             ), | |||
|  |           ), | |||
|  |           const Divider(height: 1), | |||
|  |           // footer: 取消 / 确定(固定在底部)
 | |||
|  |           Container( | |||
|  |             height: _footerHeight, | |||
|  |             padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | |||
|  |             child: Row( | |||
|  |               children: [ | |||
|  |                 Expanded( | |||
|  |                   child: CustomButton( | |||
|  |                     text: '取消', | |||
|  |                     backgroundColor: Colors.grey.shade200, | |||
|  |                     textStyle: TextStyle(fontSize: 14, color: Colors.black), | |||
|  |                     onPressed: () { | |||
|  |                       Navigator.of(context).pop(null); | |||
|  |                     }, | |||
|  |                   ), | |||
|  |                 ), | |||
|  |                 const SizedBox(width: 12), | |||
|  |                 Expanded( | |||
|  |                   child: CustomButton( | |||
|  |                     text: '确定', | |||
|  |                     backgroundColor: Colors.blue, | |||
|  |                     onPressed: () { | |||
|  |                       if (!widget.allowEmpty && _selected.isEmpty) { | |||
|  |                         ToastUtil.showNormal(context, '请至少选择一项'); | |||
|  |                         return; | |||
|  |                       } | |||
|  |                       final result = _selected | |||
|  |                           .map((i) => items[i]) | |||
|  |                           .toList(growable: false); | |||
|  |                       Navigator.of(context).pop(result); | |||
|  |                     }, | |||
|  |                   ), | |||
|  |                 ), | |||
|  |               ], | |||
|  |             ), | |||
|  |           ), | |||
|  |         ], | |||
|  |       ), | |||
|  |     ); | |||
|  |   } | |||
|  | } |