import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; /// 选择结果(单选) class PickerResult { final T value; final int index; PickerResult(this.value, this.index); } /// 选择结果(多选) class MultiPickerResult { final List values; final List indices; MultiPickerResult(this.values, this.indices); } /// 显示样式 enum BottomPickerStyle { wheel, list, } /// 通用底部弹窗选择器 class BottomPicker { static Future show( BuildContext context, { required List items, required Widget Function(T item) itemBuilder, int initialIndex = 0, double itemExtent = 50.0, double height = 250, bool withIndex = false, BottomPickerStyle style = BottomPickerStyle.wheel, bool multiSelect = false, List? initialSelectedIndices, /// 新增:列表样式标题 String title = '请选择', String cancelText = '取消', String confirmText = '确定', int maxLines = 3, }) { if (items.isEmpty) return Future.value(null); final safeIndex = initialIndex.clamp(0, items.length - 1); final initialMulti = (initialSelectedIndices ?? const []) .where((e) => e >= 0 && e < items.length) .toSet(); if (style == BottomPickerStyle.wheel) { T selected = items[safeIndex]; int selectedIndex = safeIndex; return showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), builder: (ctx) { return SizedBox( height: height, child: Column( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: () { FocusScope.of(context).unfocus(); Navigator.of(ctx).pop(); }, child: Text( cancelText, style: const TextStyle( color: Colors.black54, fontSize: 16, ), ), ), TextButton( onPressed: () { FocusScope.of(context).unfocus(); if (withIndex) { Navigator.of(ctx).pop( PickerResult(selected, selectedIndex), ); } else { Navigator.of(ctx).pop(selected); } }, child: Text( confirmText, style: const TextStyle( color: Colors.blue, fontSize: 16, ), ), ), ], ), ), const Divider(height: 1), Expanded( child: CupertinoPicker( scrollController: FixedExtentScrollController( initialItem: safeIndex, ), itemExtent: itemExtent, onSelectedItemChanged: (index) { selected = items[index]; selectedIndex = index; }, children: items.map((item) { return Center( child: SizedBox( width: double.infinity, child: DefaultTextStyle.merge( style: const TextStyle(fontSize: 16), child: itemBuilder(item), ), ), ); }).toList(), ), ), ], ), ); }, ); } // 列表样式 return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), builder: (ctx) { int selectedIndex = safeIndex; final Set selectedIndices = Set.from(initialMulti); if (!multiSelect && selectedIndices.isEmpty) { selectedIndices.add(safeIndex); } T currentSingleValue = items[selectedIndex]; return SafeArea( child: SizedBox( height: height, child: StatefulBuilder( builder: (context, setState) { return Column( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), child: Row( children: [ Expanded( child: Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), TextButton( onPressed: () { FocusScope.of(context).unfocus(); Navigator.of(ctx).pop(); }, child: Text( cancelText, style: const TextStyle( color: Colors.black54, fontSize: 16, ), ), ), TextButton( onPressed: () { FocusScope.of(context).unfocus(); if (multiSelect) { final indices = selectedIndices.toList()..sort(); final values = indices.map((e) => items[e]).toList(); if (withIndex) { Navigator.of(ctx).pop( MultiPickerResult(values, indices), ); } else { Navigator.of(ctx).pop(values); } } else { if (withIndex) { Navigator.of(ctx).pop( PickerResult(currentSingleValue, selectedIndex), ); } else { Navigator.of(ctx).pop(currentSingleValue); } } }, child: Text( confirmText, style: const TextStyle( color: Colors.blue, fontSize: 16, ), ), ), ], ), ), const Divider(height: 1), Expanded( child: ListView.separated( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: items.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final item = items[index]; final isSelected = multiSelect ? selectedIndices.contains(index) : selectedIndex == index; return InkWell( onTap: () { setState(() { if (multiSelect) { if (selectedIndices.contains(index)) { selectedIndices.remove(index); } else { selectedIndices.add(index); } } else { selectedIndex = index; currentSingleValue = item; } }); }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (multiSelect) Padding( padding: const EdgeInsets.only(top: 2), child: Icon( isSelected ? Icons.check_box : Icons.check_box_outline_blank, color: isSelected ? Colors.blue : Colors.grey, size: 22, ), ) else Padding( padding: const EdgeInsets.only(top: 2), child: Icon( isSelected ? Icons.radio_button_checked : Icons.radio_button_off, color: isSelected ? Colors.blue : Colors.grey, size: 22, ), ), const SizedBox(width: 12), Expanded( child: DefaultTextStyle.merge( style: const TextStyle( fontSize: 16, color: Colors.black87, height: 1.35, ), child: itemBuilder(item), ), ), ], ), ), ); }, ), ), ], ); }, ), ), ); }, ); } }