feat(apps/calendar): 新增日历事件创建/编辑/分享功能
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
@@ -6,8 +5,10 @@ import '../../../../core/notifications/local_notification_service.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/app_sheet_input_field.dart';
|
||||
import '../../../../shared/widgets/back_title_page_header.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import 'date_time_picker_sheet.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../../data/services/calendar_service.dart';
|
||||
|
||||
@@ -15,12 +16,14 @@ class CreateEventSheet extends StatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
final ScheduleItemModel? editingEvent;
|
||||
final VoidCallback? onSaved;
|
||||
final bool pageMode;
|
||||
|
||||
const CreateEventSheet({
|
||||
super.key,
|
||||
this.initialDate,
|
||||
this.editingEvent,
|
||||
this.onSaved,
|
||||
this.pageMode = false,
|
||||
});
|
||||
|
||||
static Future<void> show(
|
||||
@@ -135,6 +138,20 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.pageMode) {
|
||||
return Container(
|
||||
color: AppColors.background,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPageHeader(),
|
||||
_buildTabBar(),
|
||||
Expanded(child: _buildTabContent()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeOut,
|
||||
@@ -171,6 +188,44 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageHeader() {
|
||||
return BackTitlePageHeader(
|
||||
title: _isEditing ? '编辑日程' : '新建日程',
|
||||
onBack: () => Navigator.of(context).pop(),
|
||||
trailing: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _titleController,
|
||||
builder: (context, value, child) {
|
||||
final enabled = value.text.trim().isNotEmpty && !_saving;
|
||||
return SizedBox(
|
||||
height: AppSpacing.xxl * 2,
|
||||
child: TextButton(
|
||||
onPressed: enabled ? _saveEvent : null,
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
minimumSize: const Size(AppSpacing.none, AppSpacing.none),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: _saving
|
||||
? const AppLoadingIndicator(
|
||||
variant: AppLoadingVariant.button,
|
||||
size: 18,
|
||||
trackColor: AppColors.blue200,
|
||||
)
|
||||
: Text(
|
||||
'保存',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: enabled ? AppColors.blue600 : AppColors.slate400,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
height: 56,
|
||||
@@ -473,7 +528,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _DateTimePickerSheet(
|
||||
builder: (context) => DateTimePickerSheet(
|
||||
initialDate: date,
|
||||
initialTime: time,
|
||||
minTime: minTime,
|
||||
@@ -587,10 +642,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
}
|
||||
|
||||
int? _sanitizeReminderMinutes(int? minutes) {
|
||||
if (minutes == null || minutes < 0) {
|
||||
return null;
|
||||
}
|
||||
return minutes;
|
||||
return (minutes != null && minutes >= 0) ? minutes : null;
|
||||
}
|
||||
|
||||
List<int?> _buildReminderOptions() {
|
||||
@@ -692,368 +744,3 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _DateTimePickerSheet extends StatefulWidget {
|
||||
final DateTime initialDate;
|
||||
final DateTime initialTime;
|
||||
final DateTime? minTime;
|
||||
|
||||
const _DateTimePickerSheet({
|
||||
required this.initialDate,
|
||||
required this.initialTime,
|
||||
this.minTime,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_DateTimePickerSheet> createState() => _DateTimePickerSheetState();
|
||||
}
|
||||
|
||||
class _DateTimePickerSheetState extends State<_DateTimePickerSheet> {
|
||||
late int _selectedYear;
|
||||
late int _selectedMonth;
|
||||
late int _selectedDay;
|
||||
late int _selectedHour;
|
||||
late int _selectedMinute;
|
||||
|
||||
late FixedExtentScrollController _yearController;
|
||||
late FixedExtentScrollController _monthController;
|
||||
late FixedExtentScrollController _dayController;
|
||||
late FixedExtentScrollController _hourController;
|
||||
late FixedExtentScrollController _minuteController;
|
||||
|
||||
static final int _baseYear = DateTime.now().year;
|
||||
static final List<int> _years = List.generate(21, (i) => _baseYear - 10 + i);
|
||||
static final List<int> _months = List.generate(12, (i) => i + 1);
|
||||
static final List<int> _allHours = List.generate(24, (i) => i);
|
||||
static final List<int> _allMinutes = List.generate(60, (i) => i);
|
||||
|
||||
List<int> _days = [];
|
||||
late List<int> _filteredHours;
|
||||
late List<int> _filteredMinutes;
|
||||
|
||||
List<int> _getFilteredHours() {
|
||||
if (widget.minTime == null) return _allHours;
|
||||
final minDate = widget.minTime!;
|
||||
if (_selectedYear > minDate.year ||
|
||||
(_selectedYear == minDate.year && _selectedMonth > minDate.month) ||
|
||||
(_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay > minDate.day)) {
|
||||
return _allHours;
|
||||
}
|
||||
if (_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay == minDate.day) {
|
||||
return _allHours.where((h) => h > minDate.hour).toList();
|
||||
}
|
||||
return _allHours;
|
||||
}
|
||||
|
||||
List<int> _getFilteredMinutes() {
|
||||
if (widget.minTime == null) return _allMinutes;
|
||||
final minDate = widget.minTime!;
|
||||
if (_selectedYear > minDate.year ||
|
||||
(_selectedYear == minDate.year && _selectedMonth > minDate.month) ||
|
||||
(_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay > minDate.day)) {
|
||||
return _allMinutes;
|
||||
}
|
||||
if (_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay == minDate.day &&
|
||||
_selectedHour == minDate.hour) {
|
||||
return _allMinutes.where((m) => m > minDate.minute).toList();
|
||||
}
|
||||
return _allMinutes;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedYear = widget.initialDate.year;
|
||||
_selectedMonth = widget.initialDate.month;
|
||||
_selectedDay = widget.initialDate.day;
|
||||
_selectedHour = widget.initialTime.hour;
|
||||
_selectedMinute = widget.initialTime.minute;
|
||||
_filteredHours = _getFilteredHours();
|
||||
_filteredMinutes = _getFilteredMinutes();
|
||||
_updateDays();
|
||||
|
||||
_yearController = FixedExtentScrollController(
|
||||
initialItem: _years.indexOf(_selectedYear),
|
||||
);
|
||||
_monthController = FixedExtentScrollController(
|
||||
initialItem: _selectedMonth - 1,
|
||||
);
|
||||
_dayController = FixedExtentScrollController(initialItem: _selectedDay - 1);
|
||||
_hourController = FixedExtentScrollController(
|
||||
initialItem: _filteredHours.indexOf(_selectedHour),
|
||||
);
|
||||
_minuteController = FixedExtentScrollController(
|
||||
initialItem: _filteredMinutes.indexOf(_selectedMinute),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateDays() {
|
||||
_days = List.generate(
|
||||
DateTime(_selectedYear, _selectedMonth + 1, 0).day,
|
||||
(i) => i + 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_yearController.dispose();
|
||||
_monthController.dispose();
|
||||
_dayController.dispose();
|
||||
_hourController.dispose();
|
||||
_minuteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 420,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildPickerLabel('日期'),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildPicker(_years, _yearController, (v) {
|
||||
setState(() {
|
||||
_selectedYear = v;
|
||||
_updateDays();
|
||||
if (_selectedDay > _days.length) {
|
||||
_selectedDay = _days.length;
|
||||
_dayController.jumpToItem(_selectedDay - 1);
|
||||
}
|
||||
});
|
||||
}, (v) => '$v'),
|
||||
),
|
||||
const Text(
|
||||
'年',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPicker(_months, _monthController, (
|
||||
v,
|
||||
) {
|
||||
setState(() {
|
||||
_selectedMonth = v;
|
||||
_updateDays();
|
||||
if (_selectedDay > _days.length) {
|
||||
_selectedDay = _days.length;
|
||||
_dayController.jumpToItem(_selectedDay - 1);
|
||||
}
|
||||
});
|
||||
}, (v) => '$v'),
|
||||
),
|
||||
const Text(
|
||||
'月',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPicker(
|
||||
_days,
|
||||
_dayController,
|
||||
(v) => setState(() => _selectedDay = v),
|
||||
(v) => '$v',
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'日',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(width: 1, height: 180, color: AppColors.border),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildPickerLabel('时间'),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildPicker(
|
||||
_filteredHours,
|
||||
_hourController,
|
||||
(v) {
|
||||
setState(() {
|
||||
_selectedHour = v;
|
||||
_filteredMinutes = _getFilteredMinutes();
|
||||
if (_selectedMinute >
|
||||
_filteredMinutes.last) {
|
||||
_selectedMinute =
|
||||
_filteredMinutes.isNotEmpty
|
||||
? _filteredMinutes.last
|
||||
: 0;
|
||||
_minuteController.jumpToItem(
|
||||
_filteredMinutes.indexOf(
|
||||
_selectedMinute,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
(v) => v.toString().padLeft(2, '0'),
|
||||
itemExtent: 50,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
' : ',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPicker(
|
||||
_filteredMinutes,
|
||||
_minuteController,
|
||||
(v) => setState(() => _selectedMinute = v),
|
||||
(v) => v.toString().padLeft(2, '0'),
|
||||
itemExtent: 50,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(fontSize: 17, color: AppColors.slate600),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'选择时间',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context, (
|
||||
DateTime(_selectedYear, _selectedMonth, _selectedDay),
|
||||
DateTime(2000, 1, 1, _selectedHour, _selectedMinute),
|
||||
));
|
||||
},
|
||||
child: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.blue600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickerLabel(String label) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPicker(
|
||||
List<int> items,
|
||||
FixedExtentScrollController controller,
|
||||
ValueChanged<int> onChanged,
|
||||
String Function(int) formatter, {
|
||||
double itemExtent = 40,
|
||||
}) {
|
||||
return CupertinoPicker(
|
||||
scrollController: controller,
|
||||
itemExtent: itemExtent,
|
||||
magnification: 1.2,
|
||||
squeeze: 0.8,
|
||||
useMagnifier: true,
|
||||
onSelectedItemChanged: (index) => onChanged(items[index]),
|
||||
selectionOverlay: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: AppColors.blue100.withValues(alpha: 0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
children: List<Widget>.generate(items.length, (index) {
|
||||
return Center(
|
||||
child: Text(
|
||||
formatter(items[index]),
|
||||
style: const TextStyle(fontSize: 18, color: AppColors.slate900),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
|
||||
class DateTimePickerSheet extends StatefulWidget {
|
||||
final DateTime initialDate;
|
||||
final DateTime initialTime;
|
||||
final DateTime? minTime;
|
||||
|
||||
const DateTimePickerSheet({
|
||||
super.key,
|
||||
required this.initialDate,
|
||||
required this.initialTime,
|
||||
this.minTime,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DateTimePickerSheet> createState() => _DateTimePickerSheetState();
|
||||
}
|
||||
|
||||
class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
late int _selectedYear;
|
||||
late int _selectedMonth;
|
||||
late int _selectedDay;
|
||||
late int _selectedHour;
|
||||
late int _selectedMinute;
|
||||
|
||||
late FixedExtentScrollController _yearController;
|
||||
late FixedExtentScrollController _monthController;
|
||||
late FixedExtentScrollController _dayController;
|
||||
late FixedExtentScrollController _hourController;
|
||||
late FixedExtentScrollController _minuteController;
|
||||
|
||||
static final int _baseYear = DateTime.now().year;
|
||||
static final List<int> _years = List.generate(21, (i) => _baseYear - 10 + i);
|
||||
static final List<int> _months = List.generate(12, (i) => i + 1);
|
||||
static final List<int> _allHours = List.generate(24, (i) => i);
|
||||
static final List<int> _allMinutes = List.generate(60, (i) => i);
|
||||
|
||||
List<int> _days = [];
|
||||
late List<int> _filteredHours;
|
||||
late List<int> _filteredMinutes;
|
||||
|
||||
List<int> _getFilteredHours() {
|
||||
if (widget.minTime == null) return _allHours;
|
||||
final minDate = widget.minTime!;
|
||||
if (_selectedYear > minDate.year ||
|
||||
(_selectedYear == minDate.year && _selectedMonth > minDate.month) ||
|
||||
(_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay > minDate.day)) {
|
||||
return _allHours;
|
||||
}
|
||||
if (_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay == minDate.day) {
|
||||
return _allHours.where((h) => h > minDate.hour).toList();
|
||||
}
|
||||
return _allHours;
|
||||
}
|
||||
|
||||
List<int> _getFilteredMinutes() {
|
||||
if (widget.minTime == null) return _allMinutes;
|
||||
final minDate = widget.minTime!;
|
||||
if (_selectedYear > minDate.year ||
|
||||
(_selectedYear == minDate.year && _selectedMonth > minDate.month) ||
|
||||
(_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay > minDate.day)) {
|
||||
return _allMinutes;
|
||||
}
|
||||
if (_selectedYear == minDate.year &&
|
||||
_selectedMonth == minDate.month &&
|
||||
_selectedDay == minDate.day &&
|
||||
_selectedHour == minDate.hour) {
|
||||
return _allMinutes.where((m) => m > minDate.minute).toList();
|
||||
}
|
||||
return _allMinutes;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedYear = widget.initialDate.year;
|
||||
_selectedMonth = widget.initialDate.month;
|
||||
_selectedDay = widget.initialDate.day;
|
||||
_selectedHour = widget.initialTime.hour;
|
||||
_selectedMinute = widget.initialTime.minute;
|
||||
_filteredHours = _getFilteredHours();
|
||||
_filteredMinutes = _getFilteredMinutes();
|
||||
_updateDays();
|
||||
|
||||
_yearController = FixedExtentScrollController(
|
||||
initialItem: _years.indexOf(_selectedYear),
|
||||
);
|
||||
_monthController = FixedExtentScrollController(
|
||||
initialItem: _selectedMonth - 1,
|
||||
);
|
||||
_dayController = FixedExtentScrollController(initialItem: _selectedDay - 1);
|
||||
_hourController = FixedExtentScrollController(
|
||||
initialItem: _filteredHours.indexOf(_selectedHour),
|
||||
);
|
||||
_minuteController = FixedExtentScrollController(
|
||||
initialItem: _filteredMinutes.indexOf(_selectedMinute),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateDays() {
|
||||
_days = List.generate(
|
||||
DateTime(_selectedYear, _selectedMonth + 1, 0).day,
|
||||
(i) => i + 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_yearController.dispose();
|
||||
_monthController.dispose();
|
||||
_dayController.dispose();
|
||||
_hourController.dispose();
|
||||
_minuteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 420,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPickerLabel('日期'),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildPicker(_years, _yearController, (v) {
|
||||
setState(() {
|
||||
_selectedYear = v;
|
||||
_updateDays();
|
||||
if (_selectedDay > _days.length) {
|
||||
_selectedDay = _days.length;
|
||||
_dayController.jumpToItem(_selectedDay - 1);
|
||||
}
|
||||
});
|
||||
}, (v) => '$v'),
|
||||
),
|
||||
const Text(
|
||||
'年',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPicker(_months, _monthController, (
|
||||
v,
|
||||
) {
|
||||
setState(() {
|
||||
_selectedMonth = v;
|
||||
_updateDays();
|
||||
if (_selectedDay > _days.length) {
|
||||
_selectedDay = _days.length;
|
||||
_dayController.jumpToItem(_selectedDay - 1);
|
||||
}
|
||||
});
|
||||
}, (v) => '$v'),
|
||||
),
|
||||
const Text(
|
||||
'月',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPicker(
|
||||
_days,
|
||||
_dayController,
|
||||
(v) => setState(() => _selectedDay = v),
|
||||
(v) => '$v',
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'日',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(width: 1, height: 180, color: AppColors.border),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPickerLabel('时间'),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildPicker(
|
||||
_filteredHours,
|
||||
_hourController,
|
||||
(v) {
|
||||
setState(() {
|
||||
_selectedHour = v;
|
||||
_filteredMinutes = _getFilteredMinutes();
|
||||
if (_filteredMinutes.isEmpty) {
|
||||
_selectedMinute = 0;
|
||||
return;
|
||||
}
|
||||
if (_selectedMinute >
|
||||
_filteredMinutes.last) {
|
||||
_selectedMinute = _filteredMinutes.last;
|
||||
_minuteController.jumpToItem(
|
||||
_filteredMinutes.indexOf(
|
||||
_selectedMinute,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
(v) => v.toString().padLeft(2, '0'),
|
||||
itemExtent: 50,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
' : ',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPicker(
|
||||
_filteredMinutes,
|
||||
_minuteController,
|
||||
(v) => setState(() => _selectedMinute = v),
|
||||
(v) => v.toString().padLeft(2, '0'),
|
||||
itemExtent: 50,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(fontSize: 17, color: AppColors.slate600),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'选择时间',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context, (
|
||||
DateTime(_selectedYear, _selectedMonth, _selectedDay),
|
||||
DateTime(2000, 1, 1, _selectedHour, _selectedMinute),
|
||||
));
|
||||
},
|
||||
child: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.blue600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickerLabel(String label) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPicker(
|
||||
List<int> items,
|
||||
FixedExtentScrollController controller,
|
||||
ValueChanged<int> onChanged,
|
||||
String Function(int) formatter, {
|
||||
double itemExtent = 40,
|
||||
}) {
|
||||
return CupertinoPicker(
|
||||
scrollController: controller,
|
||||
itemExtent: itemExtent,
|
||||
magnification: 1.2,
|
||||
squeeze: 0.8,
|
||||
useMagnifier: true,
|
||||
onSelectedItemChanged: (index) => onChanged(items[index]),
|
||||
selectionOverlay: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: AppColors.blue100.withValues(alpha: 0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
children: List<Widget>.generate(items.length, (index) {
|
||||
return Center(
|
||||
child: Text(
|
||||
formatter(items[index]),
|
||||
style: const TextStyle(fontSize: 18, color: AppColors.slate900),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user