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);
 | ||
|                     },
 | ||
|                   ),
 | ||
|                 ),
 | ||
|               ],
 | ||
|             ),
 | ||
|           ),
 | ||
|         ],
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| }
 |