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>
This commit is contained in:
wangjia
2026-07-04 17:20:03 +08:00
parent 6d58ace4a7
commit 0425939d7e
15 changed files with 151 additions and 92 deletions
+4 -40
View File
@@ -38,7 +38,7 @@ class _DatePickerFieldState extends State<DatePickerField> {
_focus.addListener(() {
// 失焦时把输入归一化显示(如 2024 → 2024-01-01
if (!_focus.hasFocus) {
final n = _normalize(_ctrl.text);
final n = normalizeYmd(_ctrl.text);
if (n != null && n != _ctrl.text) {
_ctrl.text = n;
}
@@ -64,45 +64,9 @@ class _DatePickerFieldState extends State<DatePickerField> {
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();
parseYmd(normalizeYmd(_ctrl.text) ?? widget.value) ?? DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: init,
@@ -123,7 +87,7 @@ class _DatePickerFieldState extends State<DatePickerField> {
return FormField<String>(
initialValue: widget.value,
validator: widget.isRequired
? (_) => _normalize(_ctrl.text) == null ? '请输入日期' : null
? (_) => normalizeYmd(_ctrl.text) == null ? '请输入日期' : null
: null,
builder: (field) {
return TextField(
@@ -148,7 +112,7 @@ class _DatePickerFieldState extends State<DatePickerField> {
),
),
onChanged: (v) {
final n = _normalize(v);
final n = normalizeYmd(v);
field.didChange(n);
widget.onChanged(n);
},