flutter_integrated_whb/lib/customWidget/picker/CupertinoDatePicker.dart

383 lines
12 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,
/// 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 = false,
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,
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<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 的较大者
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<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);
}
});
}
// 检查并限制时间(模式感知)
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<String> items,
required ValueChanged<int> onSelected,
}) {
return Expanded(
child: CupertinoPicker.builder(
scrollController: controller,
itemExtent: 32,
childCount: items.length,
onSelectedItemChanged: onSelected,
itemBuilder: (context, index) {
return Center(child: Text(items[index]));
},
),
);
}
}