0425939d7e
- DsDateCell 由纯点选改为可键入:支持 2024 / 2024-5-12 / 20240512 等, 失焦或回车归一为 yyyy-MM-dd;回车提交并推进下一格(键盘流保留 Tab/Esc); 日历图标仍打开滚轮面板,滚轮确定回填输入框 - 归一词法抽为共享 normalizeYmd(core/utils/date_util.dart), DatePickerField 改为复用同一实现(登记收支等处词法一致) - 出入库表单 golden 基准随日期格形态更新(桌面+移动 ×3 主题) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
3.8 KiB
Dart
124 lines
3.8 KiB
Dart
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<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 = normalizeYmd(_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();
|
||
}
|
||
|
||
Future<void> _pickFromCalendar(FormFieldState<String> field) async {
|
||
final init =
|
||
parseYmd(normalizeYmd(_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
|
||
? (_) => normalizeYmd(_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 = normalizeYmd(v);
|
||
field.didChange(n);
|
||
widget.onChanged(n);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|