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