import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; /// 调用示例: /// DateTime? picked = await BottomDateTimePicker.showDate( /// context, /// mode: BottomPickerMode.date, // 或 BottomPickerMode.dateTime(默认)或 BottomPickerMode.dateTimeWithSeconds /// allowFuture: true, /// allowPast: false, // 是否允许选择过去(false 表示只能选择现在或未来) /// minTimeStr: '2025-08-20 08:30:45', /// ); /// if (picked != null) { /// print('用户选择的时间:$picked'); /// } enum BottomPickerMode { dateTime, date, dateTimeWithSeconds } class BottomDateTimePicker { static Future showDate( BuildContext context, { bool allowFuture = true, bool allowPast = true, // 是否允许选择过去(默认允许) String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm:ss' BottomPickerMode mode = BottomPickerMode.dateTime, }) { return showModalBottomSheet( context: context, backgroundColor: Colors.white, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), builder: (_) => _InlineDateTimePickerContent( allowFuture: allowFuture, allowPast: allowPast, minTimeStr: minTimeStr, mode: mode, ), ); } } class _InlineDateTimePickerContent extends StatefulWidget { final bool allowFuture; final bool allowPast; final String? minTimeStr; final BottomPickerMode mode; const _InlineDateTimePickerContent({ Key? key, this.allowFuture = true, this.allowPast = true, this.minTimeStr, this.mode = BottomPickerMode.dateTime, }) : 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); final List seconds = 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 FixedExtentScrollController secondCtrl; // 新增秒控制器 // 当前选中值 late int selectedYear; late int selectedMonth; late int selectedDay; late int selectedHour; late int selectedMinute; late int selectedSecond; // 新增秒选中值 DateTime? _minTime; // 解析后的最小允许时间(如果有) @override void initState() { super.initState(); // 解析 minTimeStr(若提供) _minTime = _parseMinTime(widget.minTimeStr); // 初始时间:取 now 与 _minTime 的较大者(但要考虑 allowPast) final now = DateTime.now(); DateTime initial = now; // 如果指定了最小时间并且比 now 晚,则以最小时间为初始 if (_minTime != null && _minTime!.isAfter(initial)) { initial = _minTime!; } // 如果不允许选择过去,则确保 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; } } // 根据模式调整初始值 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); } // dateTimeWithSeconds 模式保持完整的时间 selectedYear = initial.year; selectedMonth = initial.month; selectedDay = initial.day; selectedHour = initial.hour; selectedMinute = initial.minute; selectedSecond = initial.second; // 初始化天数列表 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)); secondCtrl = FixedExtentScrollController( // 初始化秒控制器 initialItem: selectedSecond.clamp(0, seconds.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:ss' 返回 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, 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; } } // 更新天数列表并调整选中日期 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); } }); } // 检查并限制时间(模式感知),支持 allowPast 与 allowFuture void _enforceConstraintsAndUpdateControllers() { 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); } // 处理最小时间约束:结合 _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; } // 如果用户也指定了 _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); } } } 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); } } 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; } } else { selectedHour = 0; selectedMinute = 0; selectedSecond = 0; } _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); } } return; } // 处理禁止选择未来(当 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; } 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; } } else { selectedHour = 0; selectedMinute = 0; selectedSecond = 0; } _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); } } return; } } } @override void dispose() { yearCtrl.dispose(); monthCtrl.dispose(); dayCtrl.dispose(); hourCtrl.dispose(); minuteCtrl.dispose(); secondCtrl.dispose(); // 释放秒控制器 super.dispose(); } @override Widget build(BuildContext context) { final isDateOnly = widget.mode == BottomPickerMode.date; final isDateTimeOnly = widget.mode == BottomPickerMode.dateTime; // 根据模式计算高度 final height = isDateOnly ? 280 : (isDateTimeOnly ? 330 : 380); return SizedBox( height: height.toDouble(), 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, ); } 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(() { final safeIdx = idx.clamp(0, days.length - 1); selectedDay = days[safeIdx]; _enforceConstraintsAndUpdateControllers(); }); }, ), // 若不是 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(); }); }, ), ], ), ), ], ), ); } Widget _buildPicker({ required FixedExtentScrollController controller, required List items, required ValueChanged onSelected, }) { return Expanded( child: CupertinoPicker.builder( scrollController: controller, itemExtent: 40, childCount: items.length, onSelectedItemChanged: onSelected, itemBuilder: (context, index) { return Center(child: Text(items[index])); }, ), ); } }