flutter_integrated_whb/lib/customWidget/picker/CupertinoDatePicker.dart

499 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<DateTime?> showDate(
BuildContext context, {
bool allowFuture = true,
bool allowPast = true, // 是否允许选择过去(默认允许)
String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm:ss'
BottomPickerMode mode = BottomPickerMode.dateTime,
}) {
return showModalBottomSheet<DateTime>(
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<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); // 新增秒数据源
// 动态天数列表(根据年月变化)
late List<int> 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<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(' ');
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<String> items,
required ValueChanged<int> onSelected,
}) {
return Expanded(
child: CupertinoPicker.builder(
scrollController: controller,
itemExtent: 40,
childCount: items.length,
onSelectedItemChanged: onSelected,
itemBuilder: (context, index) {
return Center(child: Text(items[index]));
},
),
);
}
}