import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; /// 调用示例: /// DateTime? picked = await BottomDateTimePicker.showDate( /// context, /// mode: BottomPickerMode.date, // 或 BottomPickerMode.dateTime(默认) /// allowFuture: true, /// minTimeStr: '2025-08-20 08:30', /// ); /// if (picked != null) { /// print('用户选择的时间:$picked'); /// } enum BottomPickerMode { dateTime, date } class BottomDateTimePicker { static Future showDate( BuildContext context, { bool allowFuture = false, String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm' 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, minTimeStr: minTimeStr, mode: mode, ), ); } } class _InlineDateTimePickerContent extends StatefulWidget { final bool allowFuture; final String? minTimeStr; final BottomPickerMode mode; const _InlineDateTimePickerContent({ Key? key, this.allowFuture = false, 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); // 动态天数列表(根据年月变化) 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); // 初始时间:取 now 与 _minTime 的较大者 final now = DateTime.now(); DateTime initial = now; if (_minTime != null && _minTime!.isAfter(initial)) { initial = _minTime!; } // 如果是 date 模式,只保留日期部分(时分归零) if (widget.mode == BottomPickerMode.date) { initial = DateTime(initial.year, initial.month, initial.day); } 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); } }); } // 检查并限制时间(模式感知) void _enforceConstraintsAndUpdateControllers() { final now = DateTime.now(); final isDateOnly = widget.mode == BottomPickerMode.date; final DateTime picked = isDateOnly ? DateTime(selectedYear, selectedMonth, selectedDay) : DateTime( selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute); // 处理 _minTime(如果存在),在 date 模式下只比较日期部分 if (_minTime != null) { final DateTime minRef = isDateOnly ? DateTime(_minTime!.year, _minTime!.month, _minTime!.day) : _minTime!; if (picked.isBefore(minRef)) { // 把选中项调整为 minRef selectedYear = minRef.year; selectedMonth = minRef.month; selectedDay = minRef.day; if (!isDateOnly) { selectedHour = minRef.hour; selectedMinute = minRef.minute; } else { selectedHour = 0; selectedMinute = 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); } return; } } // 处理禁止选择未来(当 allowFuture == false) if (!widget.allowFuture) { final DateTime nowRef = isDateOnly ? DateTime(now.year, now.month, now.day) : now; if (picked.isAfter(nowRef)) { selectedYear = nowRef.year; selectedMonth = nowRef.month; selectedDay = nowRef.day; if (!isDateOnly) { selectedHour = nowRef.hour; selectedMinute = nowRef.minute; } else { selectedHour = 0; selectedMinute = 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); } return; } } } @override void dispose() { yearCtrl.dispose(); monthCtrl.dispose(); dayCtrl.dispose(); hourCtrl.dispose(); minuteCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isDateOnly = widget.mode == BottomPickerMode.date; return SizedBox( height: isDateOnly ? 280 : 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 = isDateOnly ? DateTime(selectedYear, selectedMonth, selectedDay) : DateTime( selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute, ); Navigator.of(context).pop(result); }, child: const Text("确定", style: TextStyle(color: Colors.blue)), ), ], ), ), const Divider(height: 1), // 可见的滚轮列(date 模式只显示 年 月 日) 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(); }); }, ), ], ), ), ], ), ); } 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])); }, ), ); } }