flutter_integrated_whb/lib/customWidget/picker/CupertinoDatePicker.dart

413 lines
13 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默认
/// allowFuture: true,
/// allowPast: false, // 是否允许选择过去false 表示只能选择现在或未来)
/// minTimeStr: '2025-08-20 08:30',
/// );
/// if (picked != null) {
/// print('用户选择的时间:$picked');
/// }
enum BottomPickerMode { dateTime, date }
class BottomDateTimePicker {
static Future<DateTime?> showDate(
BuildContext context, {
bool allowFuture = true,
bool allowPast = true, // 新增:是否允许选择过去(默认允许)
String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm'
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);
// 动态天数列表(根据年月变化)
late List<int> 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 的较大者(但要考虑 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;
}
}
// 如果是 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<int> _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);
}
});
}
// 检查并限制时间(模式感知),支持 allowPast 与 allowFuture
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 与 allowPast
DateTime? minRef;
if (!widget.allowPast) {
// 不允许选择过去:最小时间至少为 now或当天 00:00
minRef = isDateOnly
? DateTime(now.year, now.month, now.day)
: now;
// 如果用户也指定了 _minTime 且比 now 晚,则以 _minTime 为准
if (_minTime != null && _minTime!.isAfter(minRef)) {
minRef = isDateOnly
? DateTime(_minTime!.year, _minTime!.month, _minTime!.day)
: _minTime;
}
} else if (_minTime != null) {
// 允许选择过去,但若指定了 _minTime则以 _minTime 为最小参考
minRef = isDateOnly
? DateTime(_minTime!.year, _minTime!.month, _minTime!.day)
: _minTime;
}
if (minRef != null && 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<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]));
},
),
);
}
}