flutter_integrated_whb/lib/customWidget/picker/CupertinoDatePicker.dart

499 lines
17 KiB
Dart
Raw Normal View History

2025-07-28 14:22:07 +08:00
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// 调用示例:
2025-08-14 18:14:15 +08:00
/// DateTime? picked = await BottomDateTimePicker.showDate(
/// context,
/// mode: BottomPickerMode.date, // 或 BottomPickerMode.dateTime默认或 BottomPickerMode.dateTimeWithSeconds
2025-08-29 09:52:48 +08:00
/// allowFuture: true,
2025-09-01 17:25:55 +08:00
/// allowPast: false, // 是否允许选择过去false 表示只能选择现在或未来)
/// minTimeStr: '2025-08-20 08:30:45',
2025-08-14 18:14:15 +08:00
/// );
2025-07-28 14:22:07 +08:00
/// if (picked != null) {
/// print('用户选择的时间:$picked');
/// }
enum BottomPickerMode { dateTime, date, dateTimeWithSeconds }
2025-08-29 09:52:48 +08:00
2025-07-28 14:22:07 +08:00
class BottomDateTimePicker {
static Future<DateTime?> showDate(
BuildContext context, {
2025-09-01 17:25:55 +08:00
bool allowFuture = true,
bool allowPast = true, // 是否允许选择过去(默认允许)
String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm:ss'
2025-08-29 09:52:48 +08:00
BottomPickerMode mode = BottomPickerMode.dateTime,
}) {
2025-07-28 14:22:07 +08:00
return showModalBottomSheet<DateTime>(
context: context,
backgroundColor: Colors.white,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (_) => _InlineDateTimePickerContent(
allowFuture: allowFuture,
2025-09-01 17:25:55 +08:00
allowPast: allowPast,
minTimeStr: minTimeStr,
2025-08-29 09:52:48 +08:00
mode: mode,
),
2025-07-28 14:22:07 +08:00
);
}
}
class _InlineDateTimePickerContent extends StatefulWidget {
2025-08-29 09:52:48 +08:00
final bool allowFuture;
2025-09-01 17:25:55 +08:00
final bool allowPast;
2025-08-29 09:52:48 +08:00
final String? minTimeStr;
final BottomPickerMode mode;
2025-08-14 18:14:15 +08:00
const _InlineDateTimePickerContent({
Key? key,
2025-09-01 17:25:55 +08:00
this.allowFuture = true,
this.allowPast = true,
this.minTimeStr,
2025-08-29 09:52:48 +08:00
this.mode = BottomPickerMode.dateTime,
}) : super(key: key);
2025-08-14 18:14:15 +08:00
2025-07-28 14:22:07 +08:00
@override
State<_InlineDateTimePickerContent> createState() =>
_InlineDateTimePickerContentState();
2025-07-28 14:22:07 +08:00
}
class _InlineDateTimePickerContentState
extends State<_InlineDateTimePickerContent> {
2025-07-28 14:22:07 +08:00
// 数据源
final List<int> years = List.generate(101, (i) => 1970 + i);
final List<int> months = List.generate(12, (i) => i + 1);
final List<int> hours = List.generate(24, (i) => i);
final List<int> minutes = List.generate(60, (i) => i);
final List<int> seconds = List.generate(60, (i) => i); // 新增秒数据源
2025-07-28 14:22:07 +08:00
2025-08-14 18:14:15 +08:00
// 动态天数列表(根据年月变化)
late List<int> days;
2025-07-28 14:22:07 +08:00
// Controllers
late FixedExtentScrollController yearCtrl;
late FixedExtentScrollController monthCtrl;
late FixedExtentScrollController dayCtrl;
late FixedExtentScrollController hourCtrl;
late FixedExtentScrollController minuteCtrl;
late FixedExtentScrollController secondCtrl; // 新增秒控制器
2025-07-28 14:22:07 +08:00
// 当前选中值
late int selectedYear;
late int selectedMonth;
late int selectedDay;
late int selectedHour;
late int selectedMinute;
late int selectedSecond; // 新增秒选中值
2025-07-28 14:22:07 +08:00
DateTime? _minTime; // 解析后的最小允许时间(如果有)
2025-07-28 14:22:07 +08:00
@override
void initState() {
super.initState();
// 解析 minTimeStr若提供
_minTime = _parseMinTime(widget.minTimeStr);
2025-09-01 17:25:55 +08:00
// 初始时间:取 now 与 _minTime 的较大者(但要考虑 allowPast
2025-07-28 14:22:07 +08:00
final now = DateTime.now();
DateTime initial = now;
2025-09-01 17:25:55 +08:00
// 如果指定了最小时间并且比 now 晚,则以最小时间为初始
if (_minTime != null && _minTime!.isAfter(initial)) {
initial = _minTime!;
}
2025-09-01 17:25:55 +08:00
// 如果不允许选择过去,则确保 initial 至少为 now或当天的 00:00取决于模式
if (!widget.allowPast) {
if (widget.mode == BottomPickerMode.date) {
final today = DateTime(now.year, now.month, now.day);
if (initial.isBefore(today)) initial = today;
} else {
if (initial.isBefore(now)) initial = now;
}
}
// 根据模式调整初始值
2025-08-29 09:52:48 +08:00
if (widget.mode == BottomPickerMode.date) {
initial = DateTime(initial.year, initial.month, initial.day);
} else if (widget.mode == BottomPickerMode.dateTime) {
initial = DateTime(initial.year, initial.month, initial.day,
initial.hour, initial.minute);
2025-08-29 09:52:48 +08:00
}
// dateTimeWithSeconds 模式保持完整的时间
2025-08-29 09:52:48 +08:00
selectedYear = initial.year;
selectedMonth = initial.month;
selectedDay = initial.day;
selectedHour = initial.hour;
selectedMinute = initial.minute;
selectedSecond = initial.second;
2025-07-28 14:22:07 +08:00
2025-08-14 18:14:15 +08:00
// 初始化天数列表
days = _getDaysInMonth(selectedYear, selectedMonth);
// controllers 初始项索引需在范围内
yearCtrl = FixedExtentScrollController(
initialItem: years.indexOf(selectedYear).clamp(0, years.length - 1));
2025-08-29 09:52:48 +08:00
monthCtrl = FixedExtentScrollController(
initialItem: (selectedMonth - 1).clamp(0, months.length - 1));
dayCtrl = FixedExtentScrollController(
initialItem: (selectedDay - 1).clamp(0, days.length - 1));
hourCtrl = FixedExtentScrollController(
initialItem: selectedHour.clamp(0, hours.length - 1));
minuteCtrl = FixedExtentScrollController(
initialItem: selectedMinute.clamp(0, minutes.length - 1));
secondCtrl = FixedExtentScrollController( // 初始化秒控制器
initialItem: selectedSecond.clamp(0, seconds.length - 1));
2025-08-29 09:52:48 +08:00
2025-09-01 17:25:55 +08:00
// 确保初始选择满足约束(例如 minTime 或禁止未来/禁止过去)
WidgetsBinding.instance.addPostFrameCallback((_) {
_enforceConstraintsAndUpdateControllers();
});
2025-07-28 14:22:07 +08:00
}
2025-08-14 18:14:15 +08:00
// 根据年月获取当月天数
List<int> _getDaysInMonth(int year, int month) {
final lastDay = DateUtils.getDaysInMonth(year, month);
return List.generate(lastDay, (i) => i + 1);
}
// 解析 'yyyy-MM-dd HH:mm:ss' 返回 DateTime 或 null
DateTime? _parseMinTime(String? s) {
if (s == null || s.trim().isEmpty) return null;
try {
final trimmed = s.trim();
final parts = trimmed.split(' ');
2025-08-29 09:52:48 +08:00
final dateParts =
parts[0].split('-').map((e) => int.parse(e)).toList();
final timeParts = (parts.length > 1)
? parts[1].split(':').map((e) => int.parse(e)).toList()
: [0, 0, 0];
final year = dateParts[0];
final month = dateParts[1];
final day = dateParts[2];
final hour = (timeParts.isNotEmpty) ? timeParts[0] : 0;
final minute = (timeParts.length > 1) ? timeParts[1] : 0;
final second = (timeParts.length > 2) ? timeParts[2] : 0;
return DateTime(year, month, day, hour, minute, second);
} catch (e) {
debugPrint('parseMinTime failed for "$s": $e');
return null;
}
}
2025-08-14 18:14:15 +08:00
// 更新天数列表并调整选中日期
void _updateDays({bool jumpDay = true}) {
2025-08-14 18:14:15 +08:00
final newDays = _getDaysInMonth(selectedYear, selectedMonth);
final isDayValid = selectedDay <= newDays.length;
setState(() {
days = newDays;
if (!isDayValid) {
selectedDay = newDays.last;
if (jumpDay) dayCtrl.jumpToItem(selectedDay - 1);
2025-08-14 18:14:15 +08:00
}
});
2025-07-28 14:22:07 +08:00
}
2025-09-01 17:25:55 +08:00
// 检查并限制时间(模式感知),支持 allowPast 与 allowFuture
void _enforceConstraintsAndUpdateControllers() {
2025-08-29 09:52:48 +08:00
final now = DateTime.now();
final isDateOnly = widget.mode == BottomPickerMode.date;
final isDateTimeOnly = widget.mode == BottomPickerMode.dateTime;
DateTime picked;
if (isDateOnly) {
picked = DateTime(selectedYear, selectedMonth, selectedDay);
} else if (isDateTimeOnly) {
picked = DateTime(
selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute);
} else {
picked = DateTime(selectedYear, selectedMonth, selectedDay,
selectedHour, selectedMinute, selectedSecond);
}
2025-08-29 09:52:48 +08:00
2025-09-01 17:25:55 +08:00
// 处理最小时间约束:结合 _minTime 与 allowPast
DateTime? minRef;
if (!widget.allowPast) {
// 不允许选择过去:最小时间至少为 now或当天 00:00
if (isDateOnly) {
minRef = DateTime(now.year, now.month, now.day);
} else if (isDateTimeOnly) {
minRef = DateTime(now.year, now.month, now.day, now.hour, now.minute);
} else {
minRef = now;
}
2025-09-01 17:25:55 +08:00
// 如果用户也指定了 _minTime 且比 now 晚,则以 _minTime 为准
if (_minTime != null && _minTime!.isAfter(minRef)) {
minRef = _minTime;
// 根据模式调整精度
if (isDateOnly) {
minRef = DateTime(minRef!.year, minRef.month, minRef.day);
} else if (isDateTimeOnly) {
minRef = DateTime(minRef!.year, minRef.month, minRef.day,
minRef.hour, minRef.minute);
}
2025-09-01 17:25:55 +08:00
}
} else if (_minTime != null) {
// 允许选择过去,但若指定了 _minTime则以 _minTime 为最小参考
minRef = _minTime;
// 根据模式调整精度
if (isDateOnly) {
minRef = DateTime(minRef!.year, minRef.month, minRef.day);
} else if (isDateTimeOnly) {
minRef = DateTime(minRef!.year, minRef.month, minRef.day,
minRef.hour, minRef.minute);
}
2025-09-01 17:25:55 +08:00
}
2025-08-29 09:52:48 +08:00
2025-09-01 17:25:55 +08:00
if (minRef != null && picked.isBefore(minRef)) {
// 把选中项调整为 minRef
selectedYear = minRef.year;
selectedMonth = minRef.month;
selectedDay = minRef.day;
if (!isDateOnly) {
selectedHour = minRef.hour;
selectedMinute = minRef.minute;
if (!isDateTimeOnly) {
selectedSecond = minRef.second;
} else {
selectedSecond = 0;
}
2025-09-01 17:25:55 +08:00
} else {
selectedHour = 0;
selectedMinute = 0;
selectedSecond = 0;
2025-09-01 17:25:55 +08:00
}
_updateDays(jumpDay: false);
yearCtrl.jumpToItem(years.indexOf(selectedYear));
monthCtrl.jumpToItem(selectedMonth - 1);
dayCtrl.jumpToItem(selectedDay - 1);
if (!isDateOnly) {
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
if (!isDateTimeOnly) {
secondCtrl.jumpToItem(selectedSecond);
}
2025-08-29 09:52:48 +08:00
}
2025-09-01 17:25:55 +08:00
return;
}
2025-08-14 18:14:15 +08:00
2025-08-29 09:52:48 +08:00
// 处理禁止选择未来(当 allowFuture == false
if (!widget.allowFuture) {
DateTime nowRef;
if (isDateOnly) {
nowRef = DateTime(now.year, now.month, now.day);
} else if (isDateTimeOnly) {
nowRef = DateTime(now.year, now.month, now.day, now.hour, now.minute);
} else {
nowRef = now;
}
2025-08-29 09:52:48 +08:00
if (picked.isAfter(nowRef)) {
selectedYear = nowRef.year;
selectedMonth = nowRef.month;
selectedDay = nowRef.day;
if (!isDateOnly) {
selectedHour = nowRef.hour;
selectedMinute = nowRef.minute;
if (!isDateTimeOnly) {
selectedSecond = nowRef.second;
} else {
selectedSecond = 0;
}
2025-08-29 09:52:48 +08:00
} else {
selectedHour = 0;
selectedMinute = 0;
selectedSecond = 0;
2025-08-29 09:52:48 +08:00
}
_updateDays(jumpDay: false);
yearCtrl.jumpToItem(years.indexOf(selectedYear));
monthCtrl.jumpToItem(selectedMonth - 1);
dayCtrl.jumpToItem(selectedDay - 1);
2025-08-29 09:52:48 +08:00
if (!isDateOnly) {
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
if (!isDateTimeOnly) {
secondCtrl.jumpToItem(selectedSecond);
}
2025-08-29 09:52:48 +08:00
}
return;
}
2025-07-28 14:22:07 +08:00
}
}
2025-08-14 18:14:15 +08:00
@override
void dispose() {
yearCtrl.dispose();
monthCtrl.dispose();
dayCtrl.dispose();
hourCtrl.dispose();
minuteCtrl.dispose();
secondCtrl.dispose(); // 释放秒控制器
2025-08-14 18:14:15 +08:00
super.dispose();
}
2025-07-28 14:22:07 +08:00
@override
Widget build(BuildContext context) {
2025-08-29 09:52:48 +08:00
final isDateOnly = widget.mode == BottomPickerMode.date;
final isDateTimeOnly = widget.mode == BottomPickerMode.dateTime;
// 根据模式计算高度
final height = isDateOnly ? 280 : (isDateTimeOnly ? 330 : 380);
2025-07-28 14:22:07 +08:00
return SizedBox(
height: height.toDouble(),
2025-07-28 14:22:07 +08:00
child: Column(
children: [
// 顶部按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("取消", style: TextStyle(color: Colors.grey)),
),
TextButton(
onPressed: () {
DateTime result;
if (isDateOnly) {
result = DateTime(selectedYear, selectedMonth, selectedDay);
} else if (isDateTimeOnly) {
result = DateTime(
selectedYear,
selectedMonth,
selectedDay,
selectedHour,
selectedMinute,
);
} else {
result = DateTime(
selectedYear,
selectedMonth,
selectedDay,
selectedHour,
selectedMinute,
selectedSecond,
);
}
2025-07-28 14:22:07 +08:00
Navigator.of(context).pop(result);
},
child: const Text("确定", style: TextStyle(color: Colors.blue)),
),
],
),
),
const Divider(height: 1),
// 可见的滚轮列
2025-07-28 14:22:07 +08:00
Expanded(
child: Row(
children: [
// 年
_buildPicker(
controller: yearCtrl,
items: years.map((e) => e.toString()).toList(),
onSelected: (idx) {
setState(() {
selectedYear = years[idx];
_updateDays();
_enforceConstraintsAndUpdateControllers();
2025-07-28 14:22:07 +08:00
});
},
),
// 月
_buildPicker(
controller: monthCtrl,
items: months.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedMonth = months[idx];
_updateDays();
_enforceConstraintsAndUpdateControllers();
2025-07-28 14:22:07 +08:00
});
},
),
// 日
_buildPicker(
controller: dayCtrl,
items: days.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
final safeIdx = idx.clamp(0, days.length - 1);
selectedDay = days[safeIdx];
_enforceConstraintsAndUpdateControllers();
2025-07-28 14:22:07 +08:00
});
},
),
2025-08-29 09:52:48 +08:00
// 若不是 dateOnly则显示时分两列
if (!isDateOnly)
_buildPicker(
controller: hourCtrl,
items: hours.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedHour = hours[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
if (!isDateOnly)
_buildPicker(
controller: minuteCtrl,
items: minutes.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedMinute = minutes[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
// 如果是 dateTimeWithSeconds 模式,显示秒列
if (widget.mode == BottomPickerMode.dateTimeWithSeconds)
_buildPicker(
controller: secondCtrl,
items: seconds.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedSecond = seconds[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
2025-07-28 14:22:07 +08:00
],
),
),
],
),
);
}
Widget _buildPicker({
required FixedExtentScrollController controller,
required List<String> items,
required ValueChanged<int> onSelected,
}) {
return Expanded(
child: CupertinoPicker.builder(
scrollController: controller,
2025-09-01 17:25:55 +08:00
itemExtent: 40,
2025-07-28 14:22:07 +08:00
childCount: items.length,
onSelectedItemChanged: onSelected,
itemBuilder: (context, index) {
return Center(child: Text(items[index]));
},
),
);
}
}