import 'package:flutter/material.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/i18n/app_localizations.dart'; import '../../core/theme/app_colors_extension.dart'; /// 通用日期区间选择器。 /// /// 关闭态为单个胶囊 chip(日历图标 + 日期区间文本), /// 点击弹出 TDSlidePopupRoute,内含快捷日期 chip、 /// 起始/截止 TDDatePicker、已选区间展示、取消/确定按钮。 class DateRangePicker extends StatefulWidget { final TextEditingController startCtrl; final TextEditingController endCtrl; final VoidCallback? onChanged; const DateRangePicker({ super.key, required this.startCtrl, required this.endCtrl, this.onChanged, }); @override State createState() => _DateRangePickerState(); } class _DateRangePickerState extends State { late DateTime _tempStart; late DateTime _tempEnd; @override void initState() { super.initState(); _initTemp(); } void _initTemp() { final now = DateTime.now(); _tempStart = DateTime.tryParse(widget.startCtrl.text) ?? DateTime(now.year, now.month, 1); _tempEnd = DateTime.tryParse(widget.endCtrl.text) ?? DateTime(now.year, now.month + 1, 0); } String get _displayText => '${widget.startCtrl.text} ~ ${widget.endCtrl.text}'; DateTime _today() => DateTime.now(); DateTime _dateOnly(DateTime d) => DateTime(d.year, d.month, d.day); // ---- 弹窗 ---- void _openPicker() { _initTemp(); final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; final now = DateTime.now(); Navigator.of(context).push( TDSlidePopupRoute( builder: (ctx) => _PickerContent( l10n: l10n, colors: colors, now: now, tempStart: _tempStart, tempEnd: _tempEnd, onStartChanged: (d) => _tempStart = d, onEndChanged: (d) => _tempEnd = d, onQuick: (key) { switch (key) { case 'today': final t = _dateOnly(_today()); return (start: t, end: t); case 'thisWeek': final t = _dateOnly(_today()); final wd = t.weekday; final s = t.subtract(Duration(days: wd - 1)); return (start: s, end: s.add(const Duration(days: 6))); case 'last7Days': final t = _dateOnly(_today()); return (start: t.subtract(const Duration(days: 6)), end: t); case 'thisMonth': final t = _dateOnly(_today()); return ( start: DateTime(t.year, t.month, 1), end: DateTime(t.year, t.month + 1, 0), ); case 'lastMonth': final t = _dateOnly(_today()); return ( start: DateTime(t.year, t.month - 1, 1), end: DateTime(t.year, t.month, 0), ); case 'last30Days': final t = _dateOnly(_today()); return (start: t.subtract(const Duration(days: 29)), end: t); case 'thisQuarter': final t = _dateOnly(_today()); final q = (t.month - 1) ~/ 3; return ( start: DateTime(t.year, q * 3 + 1, 1), end: DateTime(t.year, q * 3 + 4, 0), ); case 'thisYear': final t = _dateOnly(_today()); return ( start: DateTime(t.year, 1, 1), end: DateTime(t.year, 12, 31), ); default: return (start: _tempStart, end: _tempEnd); } }, onConfirm: () { widget.startCtrl.text = _fmt(_tempStart); widget.endCtrl.text = _fmt(_tempEnd); widget.onChanged?.call(); Navigator.of(ctx).pop(); }, onCancel: () => Navigator.of(ctx).pop(), ), ), ); } String _fmt(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; // ---- 关闭态 chip ---- @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final tdTheme = TDTheme.of(context); final text = _displayText; return GestureDetector( onTap: _openPicker, child: Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: colors.bgSecondaryContainer, borderRadius: BorderRadius.circular(20), border: Border.all(color: tdTheme.componentStrokeColor), ), child: Row( children: [ Icon(Icons.calendar_today, size: 16, color: colors.textSecondary), const SizedBox(width: 12), Expanded( child: Text( text, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14, color: colors.textPrimary), ), ), const SizedBox(width: 4), Icon(Icons.chevron_right, size: 16, color: colors.textSecondary), ], ), ), ); } } /// 弹窗内容(StatefulWidget,自行管理临时状态) class _PickerContent extends StatefulWidget { final AppLocalizations l10n; final AppColorsExtension colors; final DateTime now; final DateTime tempStart; final DateTime tempEnd; final void Function(DateTime d) onStartChanged; final void Function(DateTime d) onEndChanged; final ({DateTime start, DateTime end}) Function(String key) onQuick; final VoidCallback onConfirm; final VoidCallback onCancel; const _PickerContent({ required this.l10n, required this.colors, required this.now, required this.tempStart, required this.tempEnd, required this.onStartChanged, required this.onEndChanged, required this.onQuick, required this.onConfirm, required this.onCancel, }); @override State<_PickerContent> createState() => _PickerContentState(); } class _PickerContentState extends State<_PickerContent> { late DateTime _start; late DateTime _end; @override void initState() { super.initState(); _start = widget.tempStart; _end = widget.tempEnd; } String _fmt(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; List get _quickKeys => [ 'today', 'thisWeek', 'last7Days', 'thisMonth', 'lastMonth', 'last30Days', 'thisQuarter', 'thisYear', ]; String _quickLabel(String key) { switch (key) { case 'today': return widget.l10n.get('today'); case 'thisWeek': return widget.l10n.get('thisWeek'); case 'last7Days': return widget.l10n.get('last7Days'); case 'thisMonth': return widget.l10n.get('thisMonth'); case 'lastMonth': return widget.l10n.get('lastMonth'); case 'last30Days': return widget.l10n.get('last30Days'); case 'thisQuarter': return widget.l10n.get('thisQuarter'); case 'thisYear': return widget.l10n.get('thisYear'); default: return key; } } void _onQuickTap(String key) { final r = widget.onQuick(key); setState(() { _start = r.start; _end = r.end; }); } List _toList(DateTime d) => [d.year, d.month, d.day]; /// 兼容 TDDatePicker.onChange 的 List / Map 两种回调格式 List _parseSelected(dynamic selected) { if (selected is List) { return [ int.parse(selected[0].toString()), int.parse(selected[1].toString()), int.parse(selected[2].toString()), ]; } return [ int.parse(selected['year'].toString()), int.parse(selected['month'].toString()), int.parse(selected['day'].toString()), ]; } String? get _errorMessage { if (_start.isAfter(_end)) { return widget.l10n.get('filterDateStartAfterEnd'); } if (_end.difference(_start).inDays > 365) { return widget.l10n.get('dateRangeLimit'); } return null; } @override Widget build(BuildContext context) { final l10n = widget.l10n; final colors = widget.colors; final now = widget.now; return ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.9, ), child: Container( decoration: BoxDecoration( color: colors.bgPage, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 标题栏(拖拽手柄 + 标题 + 关闭按钮) _buildHeader(colors, l10n), // 内容区域(可滚动,适应不同屏幕高度) Flexible( child: SingleChildScrollView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 快捷日期 Wrap( spacing: 8, runSpacing: 8, children: _quickKeys.map((key) { return GestureDetector( onTap: () => _onQuickTap(key), child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all( color: colors.primary.withValues(alpha: 0.3), width: 0.5, ), ), child: Text( _quickLabel(key), style: TextStyle( fontSize: 13, color: colors.primary, ), ), ), ); }).toList(), ), const SizedBox(height: 24), // 起始日期 Text( l10n.get('startDateLabel'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: colors.textPrimary, ), ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration(color: colors.bgCard), child: TDDatePicker( key: ValueKey('start_${_fmt(_start)}'), header: false, model: DatePickerModel( useYear: true, useMonth: true, useDay: true, useHour: false, useMinute: false, useSecond: false, useWeekDay: false, dateStart: [2000, 1, 1], dateEnd: [now.year + 1, 12, 31], dateInitial: _toList(_start), ), onChange: (selected) { final vals = _parseSelected(selected); setState(() { _start = DateTime(vals[0], vals[1], vals[2]); }); }, ), ), ), const SizedBox(height: 24), // 截止日期 Text( l10n.get('endDateLabel'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: colors.textPrimary, ), ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration(color: colors.bgCard), child: TDDatePicker( key: ValueKey('end_${_fmt(_end)}'), header: false, model: DatePickerModel( useYear: true, useMonth: true, useDay: true, useHour: false, useMinute: false, useSecond: false, useWeekDay: false, dateStart: [2000, 1, 1], dateEnd: [now.year + 1, 12, 31], dateInitial: _toList(_end), ), onChange: (selected) { final vals = _parseSelected(selected); setState(() { _end = DateTime(vals[0], vals[1], vals[2]); }); }, ), ), ), ], ), ), ), // 校验提示 + 底部按钮 Container( decoration: BoxDecoration( color: colors.bgCard, border: Border( top: BorderSide(color: colors.border, width: 0.5), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedSize( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, child: _errorMessage != null ? Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 0), child: Row( children: [ Icon( Icons.error_outline, size: 14, color: colors.warning, ), const SizedBox(width: 6), Expanded( child: Text( _errorMessage!, style: TextStyle( fontSize: 12, color: colors.warning, ), ), ), ], ), ) : const SizedBox.shrink(), ), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), child: Row( children: [ Expanded( child: TDButton( text: l10n.get('cancel'), size: TDButtonSize.large, type: TDButtonType.outline, shape: TDButtonShape.rectangle, theme: TDButtonTheme.defaultTheme, onTap: widget.onCancel, ), ), const SizedBox(width: 12), Expanded( child: TDButton( text: l10n.get('confirm'), size: TDButtonSize.large, type: TDButtonType.fill, shape: TDButtonShape.rectangle, theme: _errorMessage != null ? TDButtonTheme.defaultTheme : TDButtonTheme.primary, onTap: _errorMessage != null ? null : () { widget.onStartChanged(_start); widget.onEndChanged(_end); widget.onConfirm(); }, ), ), ], ), ), ], ), ), ], ), ), ); } Widget _buildHeader(AppColorsExtension colors, AppLocalizations l10n) { return Column( mainAxisSize: MainAxisSize.min, children: [ Center( child: Container( margin: const EdgeInsets.only(top: 8, bottom: 4), width: 36, height: 4, decoration: BoxDecoration( color: colors.border, borderRadius: BorderRadius.circular(2), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 8, 12, 16), child: Row( children: [ const SizedBox(width: 24), Expanded( child: Center( child: Text( l10n.get('datePickerTitle'), style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), ), ), GestureDetector( onTap: widget.onCancel, child: Padding( padding: const EdgeInsets.all(4), child: Icon( Icons.close, size: 20, color: colors.textSecondary, ), ), ), ], ), ), ], ); } }