refactor(client): 日期选择统一为扁平组件——输入框+滚轮同宽居中入面板,桌面/移动全覆盖
- wheel_date_picker.dart 重构为统一组件:单日期 showDsDateDropdown(面板= 可输入框+三列滚轮+今天/确定)、范围 showDateRangeDropdown(起始|结束双窗格 并排+共用底栏);桌面锚定下拉(透明遮罩不冻结窗口)、窄屏底部 sheet 同内容 (范围纵排)——同一套窗格两端复用 - 扁平化:输入框与滚轮同宽 248、文字水平垂直居中(mono)、去掉全部阴影/立体 效果(滚轮外框仅 1px 边框,中心高亮带去描边) - DsDateCell(录单日期格)与 DatePickerField(登记收支)改为触发器:点选/ 回车打开统一面板,键入在面板内完成;出库工具栏时间 chip 补切到新组件 - 旧 showWheelDatePicker/showWheelDateRange 两步弹窗删除;组件测试改写为 新契约(触发器+面板键入+词法归一),表单 golden 随形态更新 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/theme/app_dims.g.dart';
|
||||
import '../core/theme/app_fonts.dart';
|
||||
import '../core/theme/context_tokens.dart';
|
||||
import '../core/utils/date_util.dart';
|
||||
import 'wheel_date_picker.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。
|
||||
/// 日期字段(登记收支等表单用):点选打开统一日期下拉(面板内含可输入框 +
|
||||
/// 三列滚轮,见 wheel_date_picker.dart,桌面锚定下拉 / 窄屏底部 sheet)。
|
||||
/// 值以 `yyyy-MM-dd` 字符串进出([value] / [onChanged])。
|
||||
class DatePickerField extends StatefulWidget {
|
||||
final String? value; // yyyy-MM-dd
|
||||
final ValueChanged<String?> onChanged;
|
||||
@@ -28,55 +28,12 @@ class DatePickerField extends StatefulWidget {
|
||||
}
|
||||
|
||||
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;
|
||||
Future<void> _pick(
|
||||
BuildContext anchorCtx, FormFieldState<String> field) async {
|
||||
final d =
|
||||
await showDsDateDropdown(anchorCtx, initial: parseYmd(widget.value));
|
||||
if (d != null) {
|
||||
final v = formatYmd(d);
|
||||
field.didChange(v);
|
||||
widget.onChanged(v);
|
||||
}
|
||||
@@ -84,38 +41,57 @@ class _DatePickerFieldState extends State<DatePickerField> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
return FormField<String>(
|
||||
initialValue: widget.value,
|
||||
validator: widget.isRequired
|
||||
? (_) => normalizeYmd(_ctrl.text) == null ? '请输入日期' : null
|
||||
? (_) =>
|
||||
(widget.value == null || widget.value!.isEmpty) ? '请输入日期' : 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),
|
||||
final has = widget.value != null && widget.value!.isNotEmpty;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (anchorCtx) => InkWell(
|
||||
onTap: () => _pick(anchorCtx, field),
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
child: Container(
|
||||
height: 38,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: t.surface,
|
||||
border:
|
||||
Border.all(color: field.hasError ? t.danger : t.border),
|
||||
borderRadius: BorderRadius.circular(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 ? AppFonts.mono : null,
|
||||
fontFamilyFallback:
|
||||
has ? AppFonts.monoFallback : null),
|
||||
),
|
||||
),
|
||||
Icon(LucideIcons.calendar, size: 16, color: t.muted),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (v) {
|
||||
final n = normalizeYmd(v);
|
||||
field.didChange(n);
|
||||
widget.onChanged(n);
|
||||
},
|
||||
if (field.errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, left: 2),
|
||||
child: Text(field.errorText!,
|
||||
style: TextStyle(fontSize: AppDims.fsXs, color: t.danger)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user