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:
@@ -760,7 +760,9 @@ class RowActions extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 内联日期格(.datefield / .gci-date):点选或 Enter 打开日历,值等宽显示。
|
||||
/// 内联日期格(.datefield / .gci-date):可直接键入(「2024」「2024-5-12」
|
||||
/// 「20240512」等,失焦/回车归一为 yyyy-MM-dd,同 DatePickerField 词法),
|
||||
/// 日历图标打开滚轮面板;值等宽显示。
|
||||
class DsDateCell extends StatefulWidget {
|
||||
final String? value; // yyyy-MM-dd
|
||||
final ValueChanged<String?> onChanged;
|
||||
@@ -793,9 +795,43 @@ class _DsDateCellState extends State<DsDateCell> {
|
||||
OverlayEntry? _entry;
|
||||
bool get _open => _entry != null;
|
||||
|
||||
late final TextEditingController _ctrl =
|
||||
TextEditingController(text: widget.value ?? '');
|
||||
FocusNode? _ownFocus; // 调用方没给 focusNode 时自建(自建才由本组件 dispose)
|
||||
FocusNode get _focus => widget.focusNode ?? (_ownFocus ??= FocusNode());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focus.addListener(_onFocusChange);
|
||||
}
|
||||
|
||||
void _onFocusChange() {
|
||||
// 失焦归一显示(2024 → 2024-01-01);顺带刷新激活描边
|
||||
if (!_focus.hasFocus) {
|
||||
final n = normalizeYmd(_ctrl.text);
|
||||
if (n != null && n != _ctrl.text) _ctrl.text = n;
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DsDateCell old) {
|
||||
super.didUpdateWidget(old);
|
||||
// 仅在无焦点(非用户正在输入)时同步外部值,避免回流打断输入
|
||||
if (!_focus.hasFocus &&
|
||||
old.value != widget.value &&
|
||||
(widget.value ?? '') != _ctrl.text) {
|
||||
_ctrl.text = widget.value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeOverlay();
|
||||
_focus.removeListener(_onFocusChange);
|
||||
_ownFocus?.dispose();
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -824,7 +860,9 @@ class _DsDateCellState extends State<DsDateCell> {
|
||||
child: WheelDatePanel(
|
||||
initial: init,
|
||||
onCommit: (d) {
|
||||
widget.onChanged(formatYmd(d));
|
||||
final v = formatYmd(d);
|
||||
_ctrl.text = v;
|
||||
widget.onChanged(v);
|
||||
_close();
|
||||
widget.onPicked?.call();
|
||||
},
|
||||
@@ -847,24 +885,27 @@ class _DsDateCellState extends State<DsDateCell> {
|
||||
_entry = null;
|
||||
}
|
||||
|
||||
/// 提交键入值:归一 + 回写 + 推进下一格(Enter / 失焦走这里)。
|
||||
void _commitTyped(String v) {
|
||||
final n = normalizeYmd(v);
|
||||
if (n != null) _ctrl.text = n;
|
||||
widget.onChanged(n);
|
||||
if (_open) _close();
|
||||
widget.onPicked?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
final has = widget.value != null && widget.value!.isNotEmpty;
|
||||
final height = widget.cell ? 32.0 : 38.0;
|
||||
final active = _focus.hasFocus || _open;
|
||||
return CompositedTransformTarget(
|
||||
link: _link,
|
||||
child: Focus(
|
||||
focusNode: widget.focusNode,
|
||||
// Tab/Esc 仍拦在外层(键盘流推进 / 收滚轮);Enter 交给 TextField.onSubmitted
|
||||
onKeyEvent: (node, e) {
|
||||
if (e is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
final k = e.logicalKey;
|
||||
if (k == LogicalKeyboardKey.enter ||
|
||||
k == LogicalKeyboardKey.numpadEnter ||
|
||||
k == LogicalKeyboardKey.space) {
|
||||
_toggle();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (k == LogicalKeyboardKey.escape) {
|
||||
if (_open) {
|
||||
_close();
|
||||
@@ -880,49 +921,65 @@ class _DsDateCellState extends State<DsDateCell> {
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
final active = Focus.of(context).hasFocus || _open;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.enabled
|
||||
? () {
|
||||
widget.focusNode?.requestFocus();
|
||||
_toggle();
|
||||
}
|
||||
: null,
|
||||
child: Container(
|
||||
height: height,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.cell
|
||||
? (active ? t.surface : Colors.transparent)
|
||||
: t.surface,
|
||||
border: Border.all(
|
||||
color: widget.hasError
|
||||
? t.danger
|
||||
: (active
|
||||
? t.primary
|
||||
: (widget.cell ? Colors.transparent : t.border)),
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(widget.cell ? 5 : AppDims.rMd),
|
||||
),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
has ? widget.value! : '选择日期',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
color: has ? t.text : t.faint,
|
||||
fontFamily: has ? 'monospace' : null),
|
||||
),
|
||||
),
|
||||
Icon(LucideIcons.calendar, size: 15, color: t.faint),
|
||||
]),
|
||||
child: Container(
|
||||
height: height,
|
||||
padding: const EdgeInsets.only(left: 10, right: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.cell
|
||||
? (active ? t.surface : Colors.transparent)
|
||||
: t.surface,
|
||||
border: Border.all(
|
||||
color: widget.hasError
|
||||
? t.danger
|
||||
: (active
|
||||
? t.primary
|
||||
: (widget.cell ? Colors.transparent : t.border)),
|
||||
),
|
||||
);
|
||||
}),
|
||||
borderRadius: BorderRadius.circular(widget.cell ? 5 : AppDims.rMd),
|
||||
),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _ctrl,
|
||||
focusNode: _focus,
|
||||
enabled: widget.enabled,
|
||||
keyboardType: TextInputType.datetime,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
color: t.text,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback),
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
filled: false,
|
||||
hintText: '年-月-日',
|
||||
hintStyle:
|
||||
TextStyle(fontSize: AppDims.fsBody, color: t.faint),
|
||||
),
|
||||
onChanged: (v) => widget.onChanged(normalizeYmd(v)),
|
||||
onSubmitted: _commitTyped,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.enabled
|
||||
? () {
|
||||
_focus.requestFocus();
|
||||
_toggle();
|
||||
}
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(LucideIcons.calendar, size: 15, color: t.faint),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user