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