182adca282
Deploy Client / build-client-web (push) Successful in 41s
Deploy Client / build-windows (push) Successful in 2m34s
Deploy Client / build-macos (push) Successful in 2m20s
Deploy Client / build-android (push) Successful in 1m5s
Deploy Client / build-ios (push) Successful in 2m44s
Deploy Client / release-deploy-client (push) Successful in 1m26s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
159 lines
5.0 KiB
Dart
159 lines
5.0 KiB
Dart
import 'package:flutter/material.dart';
|
||
import '../core/theme/app_theme.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<String?> onChanged;
|
||
final bool isRequired;
|
||
final String? label;
|
||
|
||
const DatePickerField({
|
||
super.key,
|
||
this.value,
|
||
required this.onChanged,
|
||
this.isRequired = false,
|
||
this.label,
|
||
});
|
||
|
||
@override
|
||
State<DatePickerField> createState() => _DatePickerFieldState();
|
||
}
|
||
|
||
class _DatePickerFieldState extends State<DatePickerField> {
|
||
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<void> _pickFromCalendar(FormFieldState<String> 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<String>(
|
||
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: const Icon(Icons.calendar_today,
|
||
size: 16, color: AppTheme.textSecondary),
|
||
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);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|