Files
jiu/client/lib/widgets/date_picker_field.dart
T
wangjia 0425939d7e feat(client): 录单日期格支持直接键入(DsDateCell 可输入 + 滚轮并存)
- 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>
2026-07-04 17:20:03 +08:00

124 lines
3.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
},
);
},
);
}
}