import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:flutter/material.dart'; import '../core/theme/context_tokens.dart'; import '../core/utils/date_util.dart'; /// 可键入日期框 + 日历图标(替代旧的「年/月/日」三下拉)。 /// /// - **直接键入**:支持「2024」「2024-5」「2024-5-12」「20240512」等,归一为 `yyyy-MM-dd`, /// 缺的月/日补 `01`(老酒只知道年份,填 2024 即 2024-01-01)。失焦时把显示归一化。 /// - **日历图标**:弹 Material `showDatePicker`(input 模式可直接键入年份,选老年份方便)。 /// - 值以 `yyyy-MM-dd` 字符串进出([value] / [onChanged]);无有效年时回调 null。 class DatePickerField extends StatefulWidget { final String? value; // yyyy-MM-dd final ValueChanged onChanged; final bool isRequired; final String? label; const DatePickerField({ super.key, this.value, required this.onChanged, this.isRequired = false, this.label, }); @override State createState() => _DatePickerFieldState(); } class _DatePickerFieldState extends State { late final TextEditingController _ctrl; final FocusNode _focus = FocusNode(); @override void initState() { super.initState(); _ctrl = TextEditingController(text: widget.value ?? ''); _focus.addListener(() { // 失焦时把输入归一化显示(如 2024 → 2024-01-01) if (!_focus.hasFocus) { final n = _normalize(_ctrl.text); if (n != null && n != _ctrl.text) { _ctrl.text = n; } } }); } @override void didUpdateWidget(DatePickerField old) { super.didUpdateWidget(old); // 仅在无焦点(非用户正在输入)时同步外部值,避免回流打断输入 if (!_focus.hasFocus && old.value != widget.value && (widget.value ?? '') != _ctrl.text) { _ctrl.text = widget.value ?? ''; } } @override void dispose() { _ctrl.dispose(); _focus.dispose(); super.dispose(); } /// 归一用户输入为 `yyyy-MM-dd`(缺月/日补 01);无有效年返回 null。 static String? _normalize(String raw) { final s = raw.trim(); if (s.isEmpty) return null; int? y, mo, d; if (RegExp(r'^[0-9]+$').hasMatch(s)) { // 纯数字串:yyyymmdd / yyyymm / yyyy if (s.length >= 8) { y = int.tryParse(s.substring(0, 4)); mo = int.tryParse(s.substring(4, 6)); d = int.tryParse(s.substring(6, 8)); } else if (s.length == 6) { y = int.tryParse(s.substring(0, 4)); mo = int.tryParse(s.substring(4, 6)); } else { y = int.tryParse(s); } } else { final parts = s.split(RegExp(r'[^0-9]+')).where((e) => e.isNotEmpty).toList(); if (parts.isNotEmpty) y = int.tryParse(parts[0]); if (parts.length >= 2) mo = int.tryParse(parts[1]); if (parts.length >= 3) d = int.tryParse(parts[2]); } if (y == null || y < 1900 || y > 2200) return null; mo ??= 1; d ??= 1; if (mo < 1 || mo > 12) return null; final maxDay = DateTime(y, mo + 1, 0).day; if (d < 1) d = 1; if (d > maxDay) d = maxDay; return '${y.toString().padLeft(4, '0')}-' '${mo.toString().padLeft(2, '0')}-' '${d.toString().padLeft(2, '0')}'; } Future _pickFromCalendar(FormFieldState field) async { final init = parseYmd(_normalize(_ctrl.text) ?? widget.value) ?? DateTime.now(); final picked = await showDatePicker( context: context, initialDate: init, firstDate: DateTime(1900), lastDate: DateTime(2200), initialEntryMode: DatePickerEntryMode.input, ); if (picked != null) { final v = formatYmd(picked); _ctrl.text = v; field.didChange(v); widget.onChanged(v); } } @override Widget build(BuildContext context) { return FormField( initialValue: widget.value, validator: widget.isRequired ? (_) => _normalize(_ctrl.text) == null ? '请输入日期' : null : null, builder: (field) { return TextField( controller: _ctrl, focusNode: _focus, keyboardType: TextInputType.datetime, style: const TextStyle(fontSize: 13), decoration: InputDecoration( isDense: true, hintText: '年-月-日', hintStyle: const TextStyle(fontSize: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), errorText: field.errorText, suffixIcon: IconButton( icon: Icon(LucideIcons.calendar, size: 16, color: context.tokens.muted), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 34, minHeight: 34), tooltip: '选择日期', onPressed: () => _pickFromCalendar(field), ), ), onChanged: (v) { final n = _normalize(v); field.didChange(n); widget.onChanged(n); }, ); }, ); } }