import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; /// 调用示例: /// DateTime? picked = await BottomDateTimePicker.showDate( /// context, /// allowFuture: true, // 添加此参数允许选择未来时间 /// minTimeStr: '2025-08-20 08:30', // 可选:不允许选择早于此时间 /// ); /// if (picked != null) { /// print('用户选择的时间:$picked'); /// } class BottomDateTimePicker { static Future showDate( BuildContext context, { bool allowFuture = false, String? minTimeStr, // 新增:可选起始时间格式 'yyyy-MM-dd HH:mm' }) { return showModalBottomSheet( context: context, backgroundColor: Colors.white, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), builder: (_) => _InlineDateTimePickerContent( allowFuture: allowFuture, minTimeStr: minTimeStr, ), ); } } class _InlineDateTimePickerContent extends StatefulWidget { final bool allowFuture; // 允许未来 final String? minTimeStr; // 新增:最小允许时间字符串 'yyyy-MM-dd HH:mm' const _InlineDateTimePickerContent({ Key? key, this.allowFuture = false, this.minTimeStr, }) : super(key: key); @override State<_InlineDateTimePickerContent> createState() => _InlineDateTimePickerContentState(); } class _InlineDateTimePickerContentState extends State<_InlineDateTimePickerContent> { // 数据源 final List years = List.generate(101, (i) => 1970 + i); final List months = List.generate(12, (i) => i + 1); final List hours = List.generate(24, (i) => i); final List minutes = List.generate(60, (i) => i); // 动态天数列表(根据年月变化) late List days; // Controllers late FixedExtentScrollController yearCtrl; late FixedExtentScrollController monthCtrl; late FixedExtentScrollController dayCtrl; late FixedExtentScrollController hourCtrl; late FixedExtentScrollController minuteCtrl; // 当前选中值 late int selectedYear; late int selectedMonth; late int selectedDay; late int selectedHour; late int selectedMinute; DateTime? _minTime; // 解析后的最小允许时间(如果有) @override void initState() { super.initState(); // 解析 minTimeStr(若提供) _minTime = _parseMinTime(widget.minTimeStr); // 选择初始时间:取 DateTime.now() 与 _minTime 的较大者(确保初始选中合法) final now = DateTime.now(); DateTime initial = now; if (_minTime != null && _minTime!.isAfter(initial)) { initial = _minTime!; } selectedYear = initial.year; selectedMonth = initial.month; selectedDay = initial.day; selectedHour = initial.hour; selectedMinute = initial.minute; // 初始化天数列表 days = _getDaysInMonth(selectedYear, selectedMonth); // controllers 初始项索引需在范围内 yearCtrl = FixedExtentScrollController( initialItem: years.indexOf(selectedYear).clamp(0, years.length - 1)); 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)); // 如果初始时间小于 minTime(理论上不会,因为我们取了较大者),再修正一次 WidgetsBinding.instance.addPostFrameCallback((_) { _enforceConstraintsAndUpdateControllers(); }); } // 根据年月获取当月天数 List _getDaysInMonth(int year, int month) { final lastDay = DateUtils.getDaysInMonth(year, month); return List.generate(lastDay, (i) => i + 1); } // 解析 'yyyy-MM-dd HH:mm' 返回 DateTime 或 null DateTime? _parseMinTime(String? s) { if (s == null || s.trim().isEmpty) return null; try { final trimmed = s.trim(); final parts = trimmed.split(' '); 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]; 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; return DateTime(year, month, day, hour, minute); } catch (e) { // 解析失败则忽略 debugPrint('parseMinTime failed for "$s": $e'); return null; } } // 更新天数列表并调整选中日期 void _updateDays({bool jumpDay = true}) { final newDays = _getDaysInMonth(selectedYear, selectedMonth); final isDayValid = selectedDay <= newDays.length; setState(() { days = newDays; if (!isDayValid) { selectedDay = newDays.last; if (jumpDay) dayCtrl.jumpToItem(selectedDay - 1); } }); } // 检查并限制时间: // 1) 优先检查 _minTime(如果存在),不允许早于 _minTime // 2) 如果没有 _minTime,则根据 allowFuture 决定是否限制到 now // 注意:_minTime 优先级高于 allowFuture(如果 _minTime 在未来,会选择 _minTime) void _enforceConstraintsAndUpdateControllers() { final picked = DateTime(selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute); // 1) 最小时间约束(优先) if (_minTime != null && picked.isBefore(_minTime!)) { final m = _minTime!; selectedYear = m.year; selectedMonth = m.month; selectedDay = m.day; selectedHour = m.hour; selectedMinute = m.minute; // 更新天数列表与控制器索引 _updateDays(jumpDay: false); yearCtrl.jumpToItem(years.indexOf(selectedYear)); monthCtrl.jumpToItem(selectedMonth - 1); dayCtrl.jumpToItem(selectedDay - 1); hourCtrl.jumpToItem(selectedHour); minuteCtrl.jumpToItem(selectedMinute); return; } // 2) 禁止选择未来(当 allowFuture == false 且没有 minTime 或 minTime <= now) if (!widget.allowFuture) { final now = DateTime.now(); // 如果 minTime 存在并大于 now,我们已在上面处理(minTime 优先),所以这里处理的是普通情况 if (picked.isAfter(now)) { selectedYear = now.year; selectedMonth = now.month; selectedDay = now.day; selectedHour = now.hour; selectedMinute = now.minute; _updateDays(jumpDay: false); yearCtrl.jumpToItem(years.indexOf(selectedYear)); monthCtrl.jumpToItem(selectedMonth - 1); dayCtrl.jumpToItem(selectedDay - 1); hourCtrl.jumpToItem(selectedHour); minuteCtrl.jumpToItem(selectedMinute); return; } } } @override void dispose() { yearCtrl.dispose(); monthCtrl.dispose(); dayCtrl.dispose(); hourCtrl.dispose(); minuteCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( height: 330, 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: () { final result = DateTime( selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute, ); Navigator.of(context).pop(result); }, child: const Text("确定", style: TextStyle(color: Colors.blue)), ), ], ), ), const Divider(height: 1), // 五列数字滚轮 Expanded( child: Row( children: [ // 年 _buildPicker( controller: yearCtrl, items: years.map((e) => e.toString()).toList(), onSelected: (idx) { setState(() { selectedYear = years[idx]; _updateDays(); _enforceConstraintsAndUpdateControllers(); }); }, ), // 月 _buildPicker( controller: monthCtrl, items: months.map((e) => e.toString().padLeft(2, '0')).toList(), onSelected: (idx) { setState(() { selectedMonth = months[idx]; _updateDays(); _enforceConstraintsAndUpdateControllers(); }); }, ), // 日 _buildPicker( controller: dayCtrl, items: days.map((e) => e.toString().padLeft(2, '0')).toList(), onSelected: (idx) { setState(() { // 防护:idx 可能超出当前 days 长度(极小概率) final safeIdx = idx.clamp(0, days.length - 1); selectedDay = days[safeIdx]; _enforceConstraintsAndUpdateControllers(); }); }, ), // 时 _buildPicker( controller: hourCtrl, items: hours.map((e) => e.toString().padLeft(2, '0')).toList(), onSelected: (idx) { setState(() { selectedHour = hours[idx]; _enforceConstraintsAndUpdateControllers(); }); }, ), // 分 _buildPicker( controller: minuteCtrl, items: minutes.map((e) => e.toString().padLeft(2, '0')).toList(), onSelected: (idx) { setState(() { selectedMinute = minutes[idx]; _enforceConstraintsAndUpdateControllers(); }); }, ), ], ), ), ], ), ); } Widget _buildPicker({ required FixedExtentScrollController controller, required List items, required ValueChanged onSelected, }) { return Expanded( child: CupertinoPicker.builder( scrollController: controller, itemExtent: 32, childCount: items.length, onSelectedItemChanged: onSelected, itemBuilder: (context, index) { return Center(child: Text(items[index])); }, ), ); } }